React - setInterval inside an onChange event - javascript

I want to set setInterval inside an onChange event, like mostly it is done on componentDidMount.
I have 2 dropdowns that filter data and then render a table, the dropdowns are controlled by onChange methods. The last on change method has a request that needs to be re-polled every X # seconds to get updated information from the server. However, these methods are outside of cDM so I'm not sure how to handle the setInterval like I previously have.
cDM() {
//axios call, update state with fetched data
}
onChange1 () {
// uses data from cDM to render the options in a dropdown.
// another axios call based on the selection, fetches data to render more options in subsequent dropdown
}
onChange2 () {
//dropdown 2 use data from onChange1 axios call. After the selection from the dropdown is made, it makes an api call that then renders data to a table.
//Some of this data updates every 5 seconds, so I need to re-poll this service to get updated information from the server.
}
if all the data was in cDM I'd normally change the data requests in cDM to an arrow function to avoid setState issues, call the function inside/outside of the setInterval callback with the following:
componentDidMount() {
this.interval = setInterval(() => this.getData(), 10000);
this.getData();
}
componentWillUnMount() {
clearInterval(this.interval);
}
getData = () => {
//data requests
}

SetInterval does not wait for the AJAX response before polling starts over again. This can become extremely buggy/memory intensive in the event of network problems.
I would suggest you use a setTimeOut and every update I would put a piece of response data in state and start the timer upon state changes inside your render function. That way you always ensure you get your result back before pounding the server again and bogging down your client's UI.

Place the code of componentDidMount body into both onChange events.
Set Interval equal to some state variable, so on componentWillUnmount you can access that interval and remove it.
onChange () {
// Change Logic
....
// Interval logic
clearInterval(this.state.interval); //Clear interval everytime.
this.state.interval = setInterval(() => this.getData(), 10000);
this.getData();
}
componentWillUnMount() {
clearInterval(this.state.interval);
}

You can indeed place this in your update event handler - event handlers do not have to be pure. There is the minor issue that doing this means that your view is no longer a function of state + props, but in reality that's not usually a problem.
It seems your main concern is how you model this data.
One way to do this might be to use a higher order component or some kind of composition, splitting the display of your component from the implementation of the business logic.
interface Props<T> {
intervalMilliseconds: number
render: React.ComponentType<ChildProps<T>>
}
interface State<T> {
// In this example I assume your select option will be some kind of string id, but you may wish to change this.
option: string | undefined
isFetching: boolean
data: T
}
interface ChildProps<T> extends State<T> {
setOption(option: string | undefined): void
}
class WithPolledData<T> extends React.Component<Props<T>, State<T>> {
interval: number
componentDidMount() {
this.setupSubscription()
}
componentDidUnmount() {
this.teardownSubscription()
}
setupSubscription() {
this.interval = setInterval(() => this.fetch(), this.props.intervalMilliseconds)
}
teardownSubscription() {
clearInterval(this.interval)
}
componentDidUpdate(previousProps: Props, previousState: State) {
if (previousProps.intervalMilliseconds !== this.props.intervalMilliseconds) {
this.teardownSubscription()
}
if (previousState.option !== this.state.option && this.state.option !== undefined) {
this.fetch()
}
}
fetch() {
if (this.props.option === undefined) {
// There is nothing to fetch
return
}
// TODO: Fetch the data from the server here with this.props.option as the id
}
setOption = (option: string | undefined) => {
this.setState({ option })
}
render() {
return React.createElement(
this.props.render,
{
...this.state,
setOption: this.setOption
}
)
}
}
You could use this component like so:
<WithPolledData intervalMilliseconds={5000} render={YourComponent} />
YourComponent would call setOption() whenever a drop down option was selected and the fetching would happen in the background.
This is how you would technically do this. I agree with #ChrisHawkes that using setInterval like this is likely a mistake - You would at the very least need to cancel in-flight HTTP requests and you run the risk of causing issues on devices with poor network performance.
Alternatively, a push rather than a pull model may be better here - This scenario is exactly what WebSockets are designed for and it wouldn't require too much modification to your code.

Related

RxJs: Update observables inside a subscription correctly

I wondered how to update a observable inside a subscription without triggering to many events.
In this example I subscribe to area: Observable<Area>, if the area changes I'd like to update the theme: Observable<Theme>. distinctUntilChanged() should do that the subscription is only triggered if the value has changed, but everytime the area gets updated, the amount of theme updates increases by one.
observeArea(): void {
this.area
.pipe(distinctUntilChanged())
.subscribe(area => {
this.themeService.updateTheme({primaryColor: area.color}); // should happen only once per area change
})
}
Is there any "correct" way of doing this, without triggering endless theme updates?
I think the problem might be with the way you are updating the theme.
If you are trying to update the theme by calling observeArea() method.
Every time you call the method a new subscription will be created.
Event will be passed to every subscription. So each time you call the method one subscription will be increasing.
Solution
Use an async pipe
area$!: Observable<any>;
observeArea(): void {
this.area$ = this.area
.pipe(
distinctUntilChanged(),
tap(area => this.themeService.updateTheme({primaryColor: area.color});)
)
}
and in html use area$ | async to subscribe.
example:
<ng-container *ngIf="area$ | async">...</ng-container>
Else you should unsubscribe every time your subscription completes
observeArea(): void {
const sub = this.area
.pipe(distinctUntilChanged())
.subscribe(area => {
this.themeService.updateTheme({primaryColor: area.color});
sub.unsubscribe();
})
}
Better to use async pipe.
I believe this solves your issue.

Vue.js component method on creation

I'm new to Vue and I'd like to make an AJAX call every time my component is rendered.
I have a vue component lets say "test-table" and Id like to fetch the contents via an AJAX call. There are many such tables and I track the active one via an v-if/v-else-if etc.
Currently I have a cheaty solution: in the template for the component I call a computed property called getData via {{ getData }} which initiates the Ajax call but does only return an empty string. Id like to switch to the proper way but dont know how.
My code is like so: (its typescript)
Vue.component("test-table", {
props: ["request"],
data () {
return {
tableData: [] as Array<TableClass>,
}
},
template: `{{ getData() }} DO SOME STUFF WITH tableData...`,
computed: {
getData() : string {
get("./foo.php", this.request, true).then(
data => this.tableData = data.map(element => new TableClass(data))
)
return "";
}
}
}
HTML:
<test-table v-if="testcounter === 1" :request="stuff...">
<test-table v-else-if="testcounter === 2" :request="other stuff...">
...
get is an async method that just sends a GET request with request data to the server. The last parameter is only for saying the method to expect a JSON as answer. Similar to JQuerys getJSON method.
the "created" method does NOT work! It fires only one time when the component is first created. If I deactivate and activate again (with v-if) the method is not called again.
Btw: I'm using Vue 2.6.13
Lifecycle hooks won't fire every time if the component is cached, keep-alive etc
Add a console.log in each of the lifecycle hooks to see.
Change to use a watcher which handles firing getData again if request changes.
...
watch: {
request: {
handler: function() {
this.getData()
},
deep: true
}
},
created() {
this.getData()
},
methods: {
getData(): string {
// do request
}
}
#FlorianBecker try the lifecycle hook updated(). It may be a better fit for what you're trying to achieve. Docs here.
You should be able to use the mounted hook if your component is continuously rendered/unrendered using v-if, like so:
export default {
mounted() {
// do ajax call here
this.callAMethod();
},
...
}
Alternatively, you could use the created() hook but it is executed earlier in the chain, so this means the DOM template is not created yet so you cant refer to it. mounted usually is the way to go.
More info on these hooks can be found here.

JS executing out of order when dispatching redux actions

I have a more complex version of the following pseudo-code. It's a React component that, in the render method, tries to get a piece of data it needs to render from a client-side read-through cache layer. If the data is present, it uses it. Otherwise, the caching layer fetches it over an API call and updates the Redux state by firing several actions (which theoretically eventually cause the component to rerender with the new data).
The problem is that for some reason it seems like after dispatching action 1, control flow moves to the top of the render function again (starting a new execution) and only way later continues to dispatch action 2. Then I again go to the top of the render, and after a while I get action 3 dispatched.
I want all the actions to fire before redux handles the rerender of the component. I would have thought dispatching an action updated the store but only forced components to update after the equivalent of a setTimeout (so at the end of the event loop), no? Is it instead the case that when you dispatch an action the component is updated synchronously immediately, before the rest of the function where the dispatch happens is executed?
class MyComponent {
render() {
const someDataINeed = CachingProvider.get(someId);
return (
<div>{someDataINeed == null ? "Loading" : someDataINeed }</div>
);
}
}
class CachingProvider {
get(id) {
if(reduxStoreFieldHasId(id)) {
return storeField[id];
}
store.dispatch(setLoadingStateForId(id));
Api.fetch().then(() => {
store.dispatch(action1);
store.dispatch(action2);
store.dispatch(action3);
});
return null;
}
}
In addition to #TrinTragula's very important answer:
This is React behaviour. Things that trigger rerenders that are invoked synchronously from an effect/lifecycle or event handler are batched, but stuff that is invoked asnychronously (see the .then in your code) will trigger a full rerender without any batching on each of those actions.
The same behaviour would apply if you would call this.setState three times in a row.
You can optimize that part by adding batch which is exported from react-redux:
Api.fetch().then(() => {
batch(() => {
store.dispatch(action1);
store.dispatch(action2);
store.dispatch(action3);
})
});
You should never invoke heavy operations inside of a render function, since it's going to be triggered way more than you would like to, slowing down your app.
You could for example try to use the useEffect hook, so that your function will be executed only when your id changes.
Example code:
function MyComponent {
useEffect(() => {
// call your method and get the result in your state
}, [someId]);
return (
<div>{someDataINeed == null ? "Loading" : someDataINeed }</div>
);
}

Best practise to handle with responses and incoming props

with redux, we uses actions to handle with crud operations. But I stuck at some points. If we send async requests inside of component. We can easly handle with response. But when we send request through actions, we dont know what happened. Is request send successfully ? it took how much amount of time ? What kind of response is returned ? we don't know that
I will clarify question with samples..
lets update a post.
onClick () {
postsApi.post(this.state.post) | we know how much time
.then(res => res.data) | has took to execute
.then(res => { | request
console.log(res) // we have the response
})
.catch(err => console.log(error))
}
But if we use actions
onClick () {
this.props.updatePost(this.state.post) // we know nothing what will happen
}
or handling with incoming props. lets say I have fetchPost() action to retrieve post
componentDidMount(){
this.props.fetchPost()
}
render method and componentDidUpdate will run as well. It's cool. But what if I want to update my state by incoming props ? I can't do this operation inside of componentDidUpdate method. it causes infinity loop.
If I use componentWillUpdate method, well, things works fine but I'm getting this warning.
Warning: componentWillReceiveProps has been renamed, and is not
recommended for use. Move data fetching code or side effects to
componentDidUpdate. If you're updating state whenever props change,
refactor your code to use memoization techniques or move it to static
getDerivedStateFromProps
I can't use componentDidUpdate method for infinty loop. Neither getDerivedStateFromProps method because it's run everytime when state change.
Should I continue to use componentWillMethod ? Otherwise what should I use and why (why componentWillMethod is unsafe ?)
If I understand correcty, what you would like to do is to safely change your local state only when your e.g. updatePost was successful.
If indeed that is your case, you can pass a callback function on your updatePost and call this as long as your update was succefull.
successfulUpdate() {
// do your thing
this.setState( ... );
}
onClick () {
this.props.updatePost(this.state.post, this.successfulUpdate) // we know nothing what will happen
}
UPDATE:
You can also keep in mind that if your action returns a promise, then you can just use the then method:
onClick () {
this.props.updatePost(this.state.post).then(this.onFulfilled, this.onRejected)
}
I think we can use redux-thunk in this cases. What if we dispatch an async function instead of dispatch an action object?
"Neither getDerivedStateFromProps method because it's run everytime when state change." - does it matter? You can avoid setting state with every getDerivedStateFromProps call by using a simple condition inside.
Example:
static getDerivedStateFromProps(props, state) {
if (props.post !== state.post) { // or anything else
return {
post: props.post,
};
}
return null;
};
An infinite loop will not occur.
Here is my way for such cases. We can redux-thunk for asynchronous calls such as api call. What if we define the action that returns promise? Please check the code below.
actions.js
export const GetTodoList = dispatch => {
return Axios.get('SOME_URL').then(res => {
// dispatch some actions
// return result
return res.data;
});
}
TodoList.js
onClick = async () => {
const { GetTodoList } = this.props;
try {
const data = await GetTodoList();
// handler for success
this.setState({
success: true,
data
});
} catch {
// handler for failure
this.setState({
success: fail,
data: null
});
}
}
const mapStateToProps = state => ({
GetTodoList
});
So we can use actions like API(which returns promise) thanks to redux-thunk.
Let me know your opinion.

Call a function that triggers a service call on every state change

I have a group of check boxes that filter the data. After the initial data is rendered and as I'm dealing with charts, I want to make the UX dynamic. Therefore, for every state change in my react component, I want to call a function that triggers a service.
handleChange = (query) => {
if(this.state.initialSearchTriggered) {
this.setState({query})
this.triggerReportsService()
}
}
Now the problem is, react takes time to update the state, and the triggerReportsService uses this.state.query to call the service. Therefore, the service query parameter does not have the latest filters. Is there a better way to do this? I was thinking to add componentDidUpdate() method but service calls are getting called multiple times than expected.
componentDidUpdate() {
this.state.initialSearchTriggered ? this.triggerReportsService() : null;
}
Please help
Thank you!
Add a callback to your setState function. The callback will fire when the state update is complete.
handleChange = (query) => {
if(this.state.initialSearchTriggered) {
this.setState({query}, this.triggerReportsService)
}
}

Categories

Resources