Using State Variable in Subscription and useEffect() in React for JavaScript - javascript

I am having a subscription, which I set up in the useEffect() Hook. Based on a variable from the store, I want to execute code (or not) which is also part of the body of the subscription.
const variableFromStore = useSelector(state => state.variableFromStore);
const dispatch = useDisptach();
useEffect(() => {
const subscriptionEvent = SomeModule.AddSubscription(event => {
console.log(variableFromStore);
if(variableFromStore) {
dispatch(foo());
}
});
})
Initially, variableFromStore is false. However, during interaction with the App (which includes unmounting the component, because the App is moved to background), it gets set to true. Then, some time later, subscriptionEvent gets fired.
But, when I console.log(variableFromStore)in there, it is always false, eventhough the redux debugger tells me it is true...
Is it not possible to pass a state/store variable into a subscription?
I assume this is because I am setting up the subscription in the useEffect() hook, which only gets executed once. But, if I set it up like this
useEffect(() => {
const subscriptionEvent = SomeModule.AddSubscription(event => {
console.log(variableFromStore);
if(variableFromStore) {
dispatch(foo());
}
});
},[variableFromStore])
Wouldn't that reinitialize the subscription every time variableFromStore changes?

This happens because your useEffect callback runs only once, and the variableFromStore is enclosed by callback closure, to fix that, redux provides another way of getting the state from the store by calling the store getState method, which returns your whole state object, so you need to import your store first, from wherever it was created, and call getState method, this way the same last value will be returned as per using the selector, but that is not affected by closure:
import store from '...'
useEffect(() => {
const subscriptionEvent = SomeModule.AddSubscription(event => {
const { variableFromStore } = store.getState()
console.log(variableFromStore);
if(variableFromStore) {
dispatch(foo());
}
});
})

If you use the useEffect with empty or without a dependency array, it will only run on the first render.
If you add the variable you want to use or run the effect on the variable's value change add that variable to the useEffect's dependency array.
Try this code down below and also check out this documentation for more.
const variableFromStore = useSelector(state => state.variableFromStore);
useEffect(() => {
const subscriptionEvent = SomeModule.AddSubscription(event => {
console.log(variableFromStore);
if(variableFromStore) {
dispatch(foo())
}
});
}, [variableFromStore])

Related

How To Stop API Request From Looping In The Network Tab [duplicate]

I've been playing around with the new hook system in React 16.7-alpha and get stuck in an infinite loop in useEffect when the state I'm handling is an object or array.
First, I use useState and initiate it with an empty object like this:
const [obj, setObj] = useState({});
Then, in useEffect, I use setObj to set it to an empty object again. As a second argument I'm passing [obj], hoping that it wont update if the content of the object hasn't changed. But it keeps updating. I guess because no matter the content, these are always different objects making React thinking it keep changing?
useEffect(() => {
setIngredients({});
}, [ingredients]);
The same is true with arrays, but as a primitive it wont get stuck in a loop, as expected.
Using these new hooks, how should I handle objects and array when checking weather the content has changed or not?
Passing an empty array as the second argument to useEffect makes it only run on mount and unmount, thus stopping any infinite loops.
useEffect(() => {
setIngredients({});
}, []);
This was clarified to me in the blog post on React hooks at https://www.robinwieruch.de/react-hooks/
Had the same problem. I don't know why they not mention this in docs. Just want to add a little to Tobias Haugen answer.
To run in every component/parent rerender you need to use:
useEffect(() => {
// don't know where it can be used :/
})
To run anything only one time after component mount(will be rendered once) you need to use:
useEffect(() => {
// do anything only one time if you pass empty array []
// keep in mind, that component will be rendered one time (with default values) before we get here
}, [] )
To run anything one time on component mount and on data/data2 change:
const [data, setData] = useState(false)
const [data2, setData2] = useState('default value for first render')
useEffect(() => {
// if you pass some variable, than component will rerender after component mount one time and second time if this(in my case data or data2) is changed
// if your data is object and you want to trigger this when property of object changed, clone object like this let clone = JSON.parse(JSON.stringify(data)), change it clone.prop = 2 and setData(clone).
// if you do like this 'data.prop=2' without cloning useEffect will not be triggered, because link to data object in momory doesn't changed, even if object changed (as i understand this)
}, [data, data2] )
How i use it most of the time:
export default function Book({id}) {
const [book, bookSet] = useState(false)
const loadBookFromServer = useCallback(async () => {
let response = await fetch('api/book/' + id)
response = await response.json()
bookSet(response)
}, [id]) // every time id changed, new book will be loaded
useEffect(() => {
loadBookFromServer()
}, [loadBookFromServer]) // useEffect will run once and when id changes
if (!book) return false //first render, when useEffect did't triggered yet we will return false
return <div>{JSON.stringify(book)}</div>
}
I ran into the same problem too once and I fixed it by making sure I pass primitive values in the second argument [].
If you pass an object, React will store only the reference to the object and run the effect when the reference changes, which is usually every singe time (I don't now how though).
The solution is to pass the values in the object. You can try,
const obj = { keyA: 'a', keyB: 'b' }
useEffect(() => {
// do something
}, [Object.values(obj)]);
or
const obj = { keyA: 'a', keyB: 'b' }
useEffect(() => {
// do something
}, [obj.keyA, obj.keyB]);
If you are building a custom hook, you can sometimes cause an infinite loop with default as follows
function useMyBadHook(values = {}) {
useEffect(()=> {
/* This runs every render, if values is undefined */
},
[values]
)
}
The fix is to use the same object instead of creating a new one on every function call:
const defaultValues = {};
function useMyBadHook(values = defaultValues) {
useEffect(()=> {
/* This runs on first call and when values change */
},
[values]
)
}
If you are encountering this in your component code the loop may get fixed if you use defaultProps instead of ES6 default values
function MyComponent({values}) {
useEffect(()=> {
/* do stuff*/
},[values]
)
return null; /* stuff */
}
MyComponent.defaultProps = {
values = {}
}
Your infinite loop is due to circularity
useEffect(() => {
setIngredients({});
}, [ingredients]);
setIngredients({}); will change the value of ingredients(will return a new reference each time), which will run setIngredients({}). To solve this you can use either approach:
Pass a different second argument to useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
setIngredients({});
}, [timeToChangeIngrediants ]);
setIngrediants will run when timeToChangeIngrediants has changed.
I'm not sure what use case justifies change ingrediants once it has been changed. But if it is the case, you pass Object.values(ingrediants) as a second argument to useEffect.
useEffect(() => {
setIngredients({});
}, Object.values(ingrediants));
As said in the documentation (https://reactjs.org/docs/hooks-effect.html), the useEffect hook is meant to be used when you want some code to be executed after every render. From the docs:
Does useEffect run after every render? Yes!
If you want to customize this, you can follow the instructions that appear later in the same page (https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects). Basically, the useEffect method accepts a second argument, that React will examine to determine if the effect has to be triggered again or not.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
You can pass any object as the second argument. If this object remains unchanged, your effect will only be triggered after the first mount. If the object changes, the effect will be triggered again.
I'm not sure if this will work for you but you could try adding .length like this:
useEffect(() => {
// fetch from server and set as obj
}, [obj.length]);
In my case (I was fetching an array!) it fetched data on mount, then again only on change and it didn't go into a loop.
If you include empty array at the end of useEffect:
useEffect(()=>{
setText(text);
},[])
It would run once.
If you include also parameter on array:
useEffect(()=>{
setText(text);
},[text])
It would run whenever text parameter change.
I often run into an infinite re-render when having a complex object as state and updating it from useRef:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients({
...ingredients,
newIngedient: { ... }
});
}, [ingredients]);
In this case eslint(react-hooks/exhaustive-deps) forces me (correctly) to add ingredients to the dependency array. However, this results in an infinite re-render. Unlike what some say in this thread, this is correct, and you can't get away with putting ingredients.someKey or ingredients.length into the dependency array.
The solution is that setters provide the old value that you can refer to. You should use this, rather than referring to ingredients directly:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients(oldIngedients => {
return {
...oldIngedients,
newIngedient: { ... }
}
});
}, []);
If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect.
I believe they are trying to express the possibility that one could be using stale data, and to be aware of this. It doesn't matter the type of values we send in the array for the second argument as long as we know that if any of those values change it will execute the effect. If we are using ingredients as part of the computation within the effect, we should include it in the array.
const [ingredients, setIngredients] = useState({});
// This will be an infinite loop, because by shallow comparison ingredients !== {}
useEffect(() => {
setIngredients({});
}, [ingredients]);
// If we need to update ingredients then we need to manually confirm
// that it is actually different by deep comparison.
useEffect(() => {
if (is(<similar_object>, ingredients) {
return;
}
setIngredients(<similar_object>);
}, [ingredients]);
The main problem is that useEffect compares the incoming value with the current value shallowly. This means that these two values compared using '===' comparison which only checks for object references and although array and object values are the same it treats them to be two different objects. I recommend you to check out my article about useEffect as a lifecycle methods.
The best way is to compare previous value with current value by using usePrevious() and _.isEqual() from Lodash.
Import isEqual and useRef. Compare your previous value with current value inside the useEffect(). If they are same do nothing else update. usePrevious(value) is a custom hook which create a ref with useRef().
Below is snippet of my code. I was facing problem of infinite loop with updating data using firebase hook
import React, { useState, useEffect, useRef } from 'react'
import 'firebase/database'
import { Redirect } from 'react-router-dom'
import { isEqual } from 'lodash'
import {
useUserStatistics
} from '../../hooks/firebase-hooks'
export function TMDPage({ match, history, location }) {
const usePrevious = value => {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
const userId = match.params ? match.params.id : ''
const teamId = location.state ? location.state.teamId : ''
const [userStatistics] = useUserStatistics(userId, teamId)
const previousUserStatistics = usePrevious(userStatistics)
useEffect(() => {
if (
!isEqual(userStatistics, previousUserStatistics)
) {
doSomething()
}
})
In case you DO need to compare the object and when it is updated here is a deepCompare hook for comparison. The accepted answer surely does not address that. Having an [] array is suitable if you need the effect to run only once when mounted.
Also, other voted answers only address a check for primitive types by doing obj.value or something similar to first get to the level where it is not nested. This may not be the best case for deeply nested objects.
So here is one that will work in all cases.
import { DependencyList } from "react";
const useDeepCompare = (
value: DependencyList | undefined
): DependencyList | undefined => {
const ref = useRef<DependencyList | undefined>();
if (!isEqual(ref.current, value)) {
ref.current = value;
}
return ref.current;
};
You can use the same in useEffect hook
React.useEffect(() => {
setState(state);
}, useDeepCompare([state]));
You could also destructure the object in the dependency array, meaning the state would only update when certain parts of the object updated.
For the sake of this example, let's say the ingredients contained carrots, we could pass that to the dependency, and only if carrots changed, would the state update.
You could then take this further and only update the number of carrots at certain points, thus controlling when the state would update and avoiding an infinite loop.
useEffect(() => {
setIngredients({});
}, [ingredients.carrots]);
An example of when something like this could be used is when a user logs into a website. When they log in, we could destructure the user object to extract their cookie and permission role, and update the state of the app accordingly.
my Case was special on encountering an infinite loop, the senario was like this:
I had an Object, lets say objX that comes from props and i was destructuring it in props like:
const { something: { somePropery } } = ObjX
and i used the somePropery as a dependency to my useEffect like:
useEffect(() => {
// ...
}, [somePropery])
and it caused me an infinite loop, i tried to handle this by passing the whole something as a dependency and it worked properly.
Another worked solution that I used for arrays state is:
useEffect(() => {
setIngredients(ingredients.length ? ingredients : null);
}, [ingredients]);

How to use componentWillReceiveProps in functional components

I'm new to functional components after spending some huge time with class components.While trying out something, I ran into some problems. How to use componentWillReceiveProps in the context of useEffect hook,
componentWillReceiveProps(nextProps) {
if (_.isEmpty(nextProps.user)) {
this.props.history.push("/signin");
}
this.setState({
selImg: nextProps.meetingData.themeImage,
});
}
Wrap the side effect of changing the route in a useEffect(), and make it dependent on user, so it would react whenever user changes. You also need to history as a dependency, but it wouldn't change.
Assign meetingData.themeImage to a const or use it directly, since the component will rerender anyway if it changes.
const Example = ({ user, history, meetingData }) => {
useEffect(() => {
if (_.isEmpty(user)) {
history.push("/signin");
}
}, [user, history]);
const selImg = meetingData.themeImage;
return (
// JSX
);
}
This could be equivalent to useEffect with dependency in functional component:
useEffect(() => {
if (_.isEmpty(props.user)) {
props.history.push("/signin");
}
setState({
selImg: props.meetingData.themeImage,
});
}, [props.user]);
Below is the functional implementation for your code.
Instead of setState you can use useState, instead of componentWillReceiveProps you can listen to updates of variables using useEffect.
Note useEffect's second argument, the dependencies array. There you can choose what updates will trigger useEffect's first variable (i.e. the callback).
function FunctionalImplementation({ user, history, meetingData }) {
// The functional equivalent of `this.state = { selImg: null }`
const [selImg, setSelImg] = useState(null);
useEffect(() => {
if (_.isEmpty(user)) {
history.push("/signin");
}
// You might want to put this inside an `else` block, just to be more clear
setSelImg(meetingData.themeImage);
// If you want to update `selImg` when `props.meetingData` changes too,
// add it to useEffect's dependencies array
}, [user, history]);
}

Mystery Parameter in setState, why does it work?

Going through a TypeScript + React course and building a todo list. My question is about a react feature though.
In the handler for adding a Todo, there is this function declared in setState
const App: React.FC= () => {
const [todos, setTodos] = useState<Todo[]>([])
const todoAddHandler = (text: string) => {
// when its called.... where does the prevTodos state come from?
setTodos(prevTodos => [...prevTodos,
{id: Math.random().toString(), text: text}])
}
return (
<div className="App">
<NewTodo onAddTodo={todoAddHandler}/>
<TodoList items={todos}></TodoList>
</div>
);
}
export default App;
When the function is called in setState, it automatically calls the current state with it. Is this just a feature of setState? That if you declare a function within it the parameter will always be the current state when the function is called?
Was very confused when this parameter just... worked. :#
TL;DR Is this just a feature of setState? - Yes
useState is a new way to use the exact same capabilities that this.state provides in a class
Meaning that its core still relies on old this.setState({}) functionality. If you remember using this.setState(), you will know that it has a callback function available, which can be used like this:
this.setState((currentState) => { /* do something with current state */ })
This has now been transfered to useState hook's second destructured item [item, setItem] setItem, thus it has the same capability:
setItem((currentState) => { /* do something with current state */ })
You can read more about it here
With hooks, React contains an internal mapping of each state name to its current value. With
const [todos, setTodos] = useState<Todo[]>([])
Whenever setTodos is called and todos state is set again, React will update the internal state for todos to the new value. It will also return the current internal state for a variable when useState is called.
You could think of it a bit like this:
// React internals
let internalState;
const setState = (param) => {
if (typeof param !== 'function') {
internalState = param;
} else {
param(internalState);
}
};
const useState = initialValue => {
internalState ??= initialValue;
return [internalState, setState];
}
Then, when you call the state setter, you can either pass it a plain value (updating internalState), or you can pass it a function that, when invoked, is passed the current internal state as the first parameter.
Note that the prevTodos parameter will contain the current state including intermediate updates. Eg, if you call setTodos twice synchronously before a re-render occurs, you'll need to use the callback form the second time in order to "see" the changes done by the first call of setTodos.

React Hook who refer to DOM element return "null" on first call

I have got an hook who catch getBoundingClientRect object of a ref DOM element. The problem is, at the first render, it return null and I need to get the value only on first render on my component.
I use it like that in a functional component:
const App = () => {
// create ref
const rootRef = useRef(null);
// get Client Rect of rootRef
const refRect = useBoundingClientRect(rootRef);
useEffect(()=> {
// return "null" the first time
// return "DOMRect" when refRect is update
console.log(refRect)
}, [refRect])
return <div ref={rootRef} >App</div>
}
Here the useBoundingClientRect hook, I call in App Component.
export function useBoundingClientRect(pRef) {
const getBoundingClientRect = useCallback(() => {
return pRef && pRef.current && pRef.current.getBoundingClientRect();
}, [pRef]);
const [rect, setRect] = useState(null);
useEffect(() => {
setRect(getBoundingClientRect());
},[]);
return rect;
}
The problem is I would like to cache boundingClientRect object on init and not the second time component is rerender :
// App Component
useEffect(()=> {
// I would like to get boundingClientRect the 1st time useEffect is call.
console.log(refRect)
// empty array allow to not re-execute the code in this useEffect
}, [])
I've check few tutorials and documentations and finds some people use useRef instead of useState hook to keep value. So I tried to use it in my useboundingClientRect hook to catch and return the boundingClientRect value on the first render of my App component. And it works... partially:
export function useBoundingClientRect(pRef) {
const getBoundingClientRect = useCallback(() => {
return pRef && pRef.current && pRef.current.getBoundingClientRect();
}, [pRef]);
const [rect, setRect] = useState(null);
// create a new ref
const rectRef = useRef(null)
useEffect(() => {
setRect(getBoundingClientRect());
// set value in ref
const rectRef = getBoundingClientRect()
},[]);
// return rectRef for the first time
return rect === null ? rectRef : rect;
}
Now the console.log(rectRef) in App Component allow to access the value on first render:
// App Component
useEffect(()=> {
console.log(refRect.current)
}, [])
But If I try to return refRect.current from useBoundingClientRect hook return null. (What?!)
if anyone can explain theses mistakes to me. Thanks in advance!
You need to understand references, mututation, and the asynchronous nature of updates here.
Firstly, when you use state to store the clientRect properties when your custom hook in useEffect runs, it sets value in state which will reflect in the next render cycle since state updates are asynchronous. This is why on first render you see undefined.
Secondly, when you are returning rectRef, you are essentially returning an object in which you later mutate when the useEffect in useBoundingClientRect runs. The data is returned before the useEffect is ran as it runs after the render cycle. Now when useEffect within the component runs, which is after the useEffect within the custom hook runs, the data is already there and has been updated at its reference by the previous useEffect and hence you see the correct data.
Lastly, if you return rectRef.current which is now a immutable value, the custom hook updates the value but at a new reference since the previous one was null and hence you don't see the change in your components useEffect method.

How to convert a React class component to a function component with hooks to get firebase data

I have a working React class component that I want to convert to a functional component to use hooks for state etc. I am learning React hooks. The class component version works fine, the functional component is where I need help.
The data structure consists of a client list with three "clients". An image of it is here:
All I am trying to do is get this data, iterate over it and display the data of each name key to the user. Simple enough.
The problem is that a call to firebase from my component leads to erratic behavior in that the data is not retrieved correctly. The last client name is continuously called and it freezes up the browser. :)
Here is an image of the result:
Here is the code:
import React, {Component,useContext,useEffect, useState} from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '#material-ui/core/styles';
import Paper from '#material-ui/core/Paper';
import Grid from '#material-ui/core/Grid';
import ListItem from '#material-ui/core/ListItem';
import Button from '#material-ui/core/Button';
import firebase from 'firebase/app';
import {Consumer,Context} from '../../PageComponents/Context';
const styles = theme => ({
root: {
flexGrow: 1,
},
paper: {
padding: theme.spacing.unit * 2,
textAlign: 'center',
color: theme.palette.text.secondary,
},
});
const FetchData = (props) =>{
const [state, setState] = useState(["hi there"]);
const userID = useContext(Context).userID;
useEffect(() => {
let clientsRef = firebase.database().ref('clients');
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key;
setState([...state, client])
});
});
//____________________________________________________BEGIN NOTE: I am emulating this code from my class component and trying to integrate it
// this.clientsRef.on('child_added', snapshot => {
// const client = snapshot.val();
// client.key = snapshot.key;
// this.setState({ clients: [...this.state.clients, client]})
// });
//___________________________________________________END NOTE
console.log(state)
return (
<ul>
{
state.map((val,index)=>{
return <a key={index} > <li>{val.name}</li> </a>
})
}
</ul>
)
}
FetchData.propTypes = {
classes: PropTypes.object.isRequired
}
export default withStyles(styles)(FetchData)
By default, useEffect callback is run after every completed render (see docs) and you're setting up a new firebase listener each such invocation. So when the Firebase emits the event each of such listeners receives the data snapshot and each of them adds to the state a received value.
Instead you need to set the listener once after component is mounted, you can do so by providing an empty array of the dependencies ([]) as a second argument to useEffect:
useEffect(() => {
// your code here
}, []) // an empty array as a second argument
This will tell React that this effect doesn't have any dependencies so there is no need to run it more than once.
But there is another one important moment. Since you setup a listener then you need to clean it up when you don't need it anymore. This is done by another callback that you should return in the function that you pass to useEffect:
useEffect(() => {
let clientsRef = firebase.database().ref('clients');
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key;
setState([...state, client])
});
return () => clientsRef.off('child_added') // unsubscribe on component unmount
}, []);
Basically this returned cleanup function will be invoked before every new effect is called and right before a component unmounts (see docs) so only this cleanup function should solve your solution by itself, but there's no need to call your effect after every render anyway hence [] as a second argument.
Your problem is that by default, useEffect() will run every single time your component renders. What is happening, is that your effect triggers a change in the component, which will trigger the effect running again and you end up with something approximating an endless loop.
Luckily react gives us some control over when to run the effect hook in the form of an array you can pass in as an additional parameter. In your case for example:
useEffect(() => {
let clientsRef = firebase.database().ref('clients');
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key;
setState([...state, client])
});
}, []);//An empty array here means this will run only once.
The array tells react which properties to watch. Whenever one of those properties changes it will run the cleanup function and re-run the effect. If you submit an empty array, then it will only run once (since there are no properties to watch). For example, if you were to add [userId] the effect would run every time the userId variable changes.
Speaking of cleanup function, you are not returning one in your effect hook. I'm not familiar enough with firebase to know if you need to clean anything up when the component is destroyed (like for example remove the 'child_added' event binding). It would be good practice to return a method as the last part of your use effect. The final code would look something like:
useEffect(() => {
let clientsRef = firebase.database().ref('clients');
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key;
setState([...state, client])
});
return () => { /* CLEANUP CODE HERE */ };
}, []);//An empty array here means this will run only once.
Effects, by default, run after every render, and setting state causes a render. Any effect that updates state needs to have a dependency array specified, otherwise you'll just have an infinite update-render-update-render loop.
Also, remember to clean up any subscriptions that effects create. Here, you can do that by returning a function which calls .off(...) and removes the listener.
Then, make sure to use the function form of state update, to make sure the next state always relies on the current state, instead of whatever the closure value happened to be when binding the event. Consider using useReducer if your component's state becomes more complex.
const [clients, setClients] = useState([])
useEffect(() => {
const clientsRef = firebase.database().ref("clients")
const handleChildAdded = (snapshot) => {
const client = snapshot.val()
client.key = snapshot.key
setClients(clients => [...clients, client])
}
clientsRef.on("child_added", handleChildAdded)
return () => clientsRef.off('child_added', handleChildAdded)
}, [])
Also see:
How to fetch data with hooks
React Firebase Hooks
A complete guide to useEffect

Categories