React Hooks: Handling Objects as dependency in useEffects - javascript

UPDATE: Yes for use case 1, if I extract search.value outside the useEffect and use it as a dependency it works.
But I have an updated Use case below
Use Case 2: I want to pass a searchHits Object to the server. The server in turn return it back to me with an updated value in response.
If I try using the searchHits Object I still get the infinite loop
state: {
visible: true,
loading: false,
search: {
value: “”,
searchHits: {....},
highlight: false,
}
}
let val = search.value
let hits = search.searchHits
useEffect( () => {
axios.post(`/search=${state.search.value}`, {hits: hits}).then( resp => {
…do something or ..do nothing
state.setState( prevState => {
return {
…prevState,
search: {... prevState.search, hits: resp.hit}
}
})
})
}, [val, hits])
Use Case 1: I want to search for a string and then highlight when I get results
e.g.
state: {
visible: true,
loading: false,
search: {
value: “”,
highlight: false,
}
}
useEffect( () => {
axios.get(`/search=${state.search.value}`).then( resp => {
…do something or ..do nothing
state.setState( prevState => {
return {
…prevState,
search: {... prevState.search, highlight: true}
}
})
})
}, [state.search])
In useEffect I make the API call using search.value.
eslint complains that there is a dependency on state.search , it does not recognize state.search.value. Even if you pass state.search.value it complains about state.search
Now if you pass state.search as dependecy it goes in an infinite loop because after the api call we are updating the highlights flag inside search.
Which will trigger another state update and a recursive loop.
One way to avoid this is to not have nested Objects in state or move the highlights flag outside search, but I am trying to not go that route give the sheer dependecies I have.
I would rather have an Object in state called search the way it is. Is there any way to better approach this.
If I want to keep my state Object as above how do I handle the infinite loop

Just a eslint stuff bug may be. You have retracted some code by saying //do something and have hidden he code. Are you sure that it doesn't have anything to do with search object?
Also, try to extract the variable out before useEffect().
const searchValue = state.search.value;
useEffect(()=>{// axios call here},[searchValue])
If your search value is an object, react does shallow comparison and it might not give desired result. Re-rendering on a set of object dependencies isn't ideal. Extract the variables.
React does shallow comparison of dependencies specified in useEffect
eg.,
const {searchParam1, searchParam2} = search.value;
useEffect(() => {
//logic goes here
}, [searchParam1, searchParam2]);
Additionally, you can add dev dependency for eslint-plugin-react-hooks, to identify common errors with hooks

Related

useEffect/useCallback missing dependency warnings, but redux state does not let me fix it

I'm trying to clean up my warnings, but im facing those dependency warnings.
This is an example, but a lot of useEffect() is facing a similar problem.
Im trying to laod my page calling my fetch api inside useCallback (got samething inside useEffect), but the filter param there is actually a redux state
useEffect(() => {
if (checkValidRoute(env.activeSelector.menu, "indicacoes")) {
dispatch(
indicationsAction.getIndications(config.page, config.rowsPerPage, config.order, {
environmentId: env[router.query.ambiente].envId,
loginId: user.login?.id,
selectorID: env.activeSelector?.selectorID,
token: user.login.token,
details: false,
filter: {
status: config.status,
dateInit: dateFormat(beforeMonth),
dateEnd: dateFormat(today),
name: config.name,
indicatorName: config.indicatorName
}
})
)
} else {
router.push(`/${router.query.ambiente}`)
}
}, [env, config.status, config.order, dispatch, beforeMonth, config.indicatorName, config.name, config.page, config.rowsPerPage, router, today, user.login?.id, user.login.token])
Those filters has it value associated to an input, i do not want to re-fetch after change my config state, because i need to wait for the user fill all the filter fields, but i need to reload my page if my env change.
I thought about this solution, but it does not work
const filterParams = {
page: config.page,
rowsPerPage: config.rowsPerPage,
order: config.order,
details: false,
filter: {
status: config.status,
dateInit: dateFormat(beforeMonth),
dateEnd: dateFormat(today),
name: config.name,
indicatorName: config.indicatorName
}
}
const loadPage = useCallback(() => {
if (checkValidRoute(env.activeSelector.menu, "indicacoes")) {
dispatch(
indicationsAction.getIndications({
environmentId: env[router.query.ambiente].envId,
loginId: user.login?.id,
selectorID: env.activeSelector?.selectorID,
token: user.login.token,
}, filterParams)
)
} else {
router.push(`/${router.query.ambiente}`)
}
}, [dispatch, env, router, user.login?.id, user.login.token, filterParams])
useEffect(() => {
loadPage()
}, [loadPage])
Now I got the following warning:
The 'filterParams' object makes the dependencies of useCallback Hook (at line 112) change on every render. Move it inside the useCallback callback. Alternatively, wrap the initialization of 'filterParams' in its own useMemo() Hook.eslintreact-hooks/exhaustive-deps
if add filterParams to useMemo() dependencies samething will happend
// eslint-disable-next-line react-hooks/exhaustive-deps sounds not good ...
There's any solution for this ? I think that I have to change my form to useForm() to get the onChange values then after submit() i set my redux state... but i dont know yet
EDIT: In that case i did understand that we need differente states to control my input state and my request state, they cant be equals. If someone find another solution, i would appreciate (:
EDIT2: Solved by that way:
const [ filtersState ] = useState(
{
page: config.page,
rowsPerPage: config.rowsPerPage,
order: config.order,
data: {
environmentId: env[router.query.ambiente].envId,
loginId: user.login?.id,
selectorID: env.activeSelector?.selectorID,
token: user.login.token,
details: false,
filter: {
status: config.status,
dateInit: dateFormat(config.dateInit),
dateEnd: dateFormat(config.dateEnd),
name: config.name,
indicatorName: config.indicatorName
}
}
}
);
const handleLoadPage = useCallback(() => {
if (checkValidRoute(env.activeSelector.menu, "indicacoes")) {
dispatch(indicationsAction.getIndications({
...filtersState,
filters: {
...filtersState.filters,
selectorID: env.activeSelector?.selectorID,
}
}))
} else {
router.push(`/${router.query.ambiente}`)
}
}, [env.activeSelector, filtersState, dispatch, router]
)
useEffect(() => {
handleLoadPage()
}, [handleLoadPage])
Any other alternatives is appreciate
The thing here is, if you memoize something, it dependencies(if are in local scope) must be memoized too.
I recommend you read this amazing article about useMemo and useCallback hooks.
To solve your problem you need to wrap filterParams within useMemo hook. And if one of it dependencies are in local scope, for example the dateFormat function, you'll need to wrap it as well.

Mui-Datatable onTableChange is not working when adding a setState

I am using mui-datatable and based on the official example of this codesandbox, you can setState on the tableState. https://codesandbox.io/s/trusting-jackson-k6t7ot?file=/examples/on-table-init/index.js
handleTableInit = (action, tableState) => {
console.log("handleTableInit: ", tableState);
this.setState({ table: tableState });
};
handleTableChange = (action, tableState) => {
console.log("handleTableChange: ", tableState);
this.setState({ table: tableState });
};
I wanted to get the the tableState.displayData hence, I added this, however, this will result to an error that says:
Maximum update depth exceeded. This can happen when a component
repeatedly calls setState inside componentWillUpdate or
componentDidUpdate. React limits the number of nested updates to
prevent infinite loops.
const handleChange = (action, tableState) => {
console.log(tableState.displayData);
setDisplayedData(tableState.displayData);
};
const options = {
enableNestedDataAccess: ".",
print: false,
filterType: "multiselect",
selectableRows: "none",
downloadOptions: { filename: "Data.csv", separator: "," },
expandableRows: true,
onTableChange: handleChange,
onTableInit: handleTableChange,
I wanted to store the data of the tableState.displayData to the setDisplayedData. How can I fix this error?
I recreated this error on codesandbox: https://codesandbox.io/s/mui-datatable-reports-mqrbb3?file=/src/App.js:4130-4415
This is keep rendering because you have used setDisplayedData in the handleChange function. so whenever table change you update the state and it again changing the state. So it is going to an infinite loop.
you should put condition to check if data you are getting is different from the prev one or not. you can try isEqualwith & isEqual functions from lodash library to check if you new data is different from old or not.
const handleChange = (action, tableState) => {
console.log(tableState.displayData);
if(!isEqualwith(displayedData, tableState.displayData, isEqual)) {
setDisplayedData([...tableState.displayData]);}
};
Note: add lodash to you dependencies and import isEqualwith & isEqual functions.

Reactivity problem with mapGetters only on first load

I am using the mapGetters helper from VueX but i have some problem only on the first load of the page, it's not reactive...
Let me show you :
my html template triggering the change :
<input type="number" value="this.inputValue" #change="this.$store.dispatch('setInputValue', $event.target.value)">
my store receiving the value
{
state: {
appValues: {
inputValue: null
},
},
getters: {
getInputValue: (state) => {
return state.appValues.inputValue;
},
},
mutations: {
setInputValue(state, value) {
state.appValues.inputValue = value;
},
},
actions: {
setInputValue(context, payload) {
return new Promise((resolve, reject) => {
context.commit('setInputValue', payload);
resolve();
});
},
}
}
and then my component listening the store :
import {mapGetters} from 'vuex';
computed: {
...mapGetters({
inputValue: 'getInputValue',
}),
}
watch: {
inputValue: {
deep: true,
immediate: true,
handler(nVal, oVal) {
console.log("inputValue", nVal, oVal);
}
},
}
So now, when i first load the page I get this console.log "inputValue" null undefined which is totally normal because as I have nothing in my store it gaves me the default value null.
But now it's the weird part. I start changing the input value and I don't have nothing appearing in my console. Nothing is moving...
Then I reload the page and on the load I get this console.log "inputValue" 5 undefined (5 is the value I entered previously) so as you can see, when I was changing the input previously, it was well keeping the value in the store but the computed value was not updating itself...
Ans now, when I change the value of the input I have my console log like this "inputValue" 7 5 so it's working as I would like it to work from the start...
What do I do wrong? Why on the first load the computed value not reactive?
Thanks for your answers...
I think the best way to solve this issue is to store a local variable with a watcher, and then update vuex when the local is changed:
On your component:
<input type="number" v-model="value">
data() {
return {
value: ''
};
},
computed: {
...mapGetters({
inputValue: 'getInputValue'
})
}
watch: {
value(value){
this.$store.dispatch('setInputValue', value);
},
inputValue(value) {
console.log('inputValue', value);
}
},
created() {
// set the initial value to be the same as the one in vuex
this.value = this.inputValue;
}
Please take a look at this sample: https://codesandbox.io/s/vuex-store-ne3ol
Your mistake is, you are using this keyword in template. One shouldn't use this in template code.
<input
type="number"
value="inputValue"
#change="$store.dispatch('setInputValue', $event.target.value)"
>
Bonus tip: It is redundant to use a getter to return the default state
if you can just use mapState to return the state.
There are a few small mistakes in the template. It should be this:
<input type="number" :value="inputValue" #change="$store.dispatch('setInputValue', $event.target.value)">
I've removed the this. in a couple of places and put a : out the front of value. Once I make these changes everything works as expected. The this.$store was causing console errors for me using Vue 2.6.10.
I would add that you're using the change event. This is the standard DOM change event and it won't fire until the field blurs. So if you just start typing you won't see anything happen in the console. You'd be better off using input if you want it to update on every keystroke. Alternatively you could use v-model with a getter and setter (see https://vuex.vuejs.org/guide/forms.html#two-way-computed-property).
My suspicion is that when you were reloading the page that was triggering the change event because it blurred the field.
Ok, so ... I found the problem and it was not relative to my examples, I can't really explain why, but I'll try to explain how :
In my store I have the next method :
mutations: {
deleteAppValues(state) {
state.appValues = null;
}
}
I was using this one on the Logout, or when the user first comes on the pageand was not logged-in... So what was going-on?
The User first load the page, the store is initializing well, and the index inputValue is initialized with null value, so it exists...
... But as the User is not logged, I destroy the store so now the inputValue is not equals to null, it just doesn't exist...
Trying to use mapGetters on something that don't exists, the reactivity won't work, so if I dispatch a change, the store key will be created, but as the mapGetters was initialized with an inexisting key, it doesn't listen the reactivity...
After reloading the page, the key now exists in the store so the getter can be attached to it and so now everything working fine...
This is exactly the explaination of what was going wrong about my code... So to make it works fine, I just changed my destruction mutation to :
mutations: {
deleteAppValues(state) {
state.appValues = {
inputValue: null,
};
}
}
Like this, the inputValue key of the store object will always exists and so the getter won't lose his reactivity...
I tryed to make a simple concise question but that made me forgot the bad part of my code, sorry.

Firebase React Binding

I'm somewhat new to React, and using the re-base library to work with Firebase.
I'm currently trying to render a table, but because of the way my data is structured in firebase, I need to get a list of keys from two locations- the first one being a list of user keys that are a member of a team, and the second being the full user information.
The team node is structured like this: /teams/team_id/userkeys, and the user info is stored like this: /Users/userkey/{email, name, etc.}
My table consists of two react components: a table component and a row component.
My table component has props teamid passed to it, and I'm using re-base's bindToState functionality to get the associated user keys in componentWillMount(). Then, I use bindToState again to get the full user node, like so:
componentWillMount() {
this.ref = base.bindToState(`/teams/${this.props.data}/members`, {
context: this,
state: 'members',
asArray: true,
then() {
this.secondref = base.bindToState('/Users', {
context: this,
state: 'users',
asArray: true,
then() {
let membersKeys = this.state.members.map(function(item) {
return item.key;
});
let usersKeys = this.state.members.map(function(item) {
return item.key;
});
let onlyCorrectMembersKeys = intersection(membersKeys, usersKeys);
this.setState({
loading: false
});
}
});
}
});
}
As you can see, I create membersKeys and usersKeys and then use underscore.js's intersection function to get all the member keys that are in my users node (note: I do this because there are some cases where a user will be a member of a team, but not be under /Users).
The part I'm struggling with is adding an additional rebase call to create the full members array (ie. the user data from /Users for the keys in onlyCorrectMembersKeys.
Edit: I've tried
let allKeys = [];
onlyCorrectMembersKeys.forEach(function(element) {
base.fetch(`/Users/${element}`, {
asArray: true,
then(data) {
allKeys.prototype.concat(data);
}
});
});
But I'm receiving the error Error: REBASE: The options argument must contain a context property of type object. Instead, got undefined
I'm assuming that's because onlyCorrectMembersKeys hasn't been fully computed yet, but I'm struggling with how to figure out the best way to solve this..
For anyone dealing with this issue as well, I seemed to have found (somewhat) of a solution:
onlyCorrectMembersKeys.map(function(item) {
base.fetch(`/Users/${item}`, {
context: this,
asObject: true,
then(data) {
if (data) {
allKeyss.push({item,data});
this.setState({allKeys: allKeyss});
}
this.setState({loading: false});
},
onFailure(err) {
console.log(err);
this.setState({loading: false});
}
})
}, this);
}
This works fine, but when users and members state is updated, it doesn't update the allkeys state. I'm sure this is just due to my level of react knowledge, so when I figure that out I'll post the solution.
Edit: using listenTo instead of bindToState is the correct approach as bindToState's callback is only fired once.

Object's data missing when passing to Meteor.call

In my React/Meteor application, I am trying to pass an object with data from the state to a method on the server, for insertion into the database. However, there seems to be an issue passing the object from the React component to the Meteor method - one of the child objects ends up in the Meteor method, but all of its children are gone. I do nothing to the object except use check() to ensure it is an Object:
'Appointments.saveData'(dataObj) {
check(dataObj, Object);
console.log(dataObj);
// ....
}
Here's what happens on the front-end:
Meteor.call('Appointments.saveData', {
vitalsData: this.state.vitalsData || {},
subjectiveData: this.state.subjectiveData || '',
physicalExamData: this.state.physicalExamData || {},
rosData: this.state.rosData || {},
impressionData: this.state.impressionData || [],
extraNotes: this.state.extraNotes || ''
}, (err, res) => {
if (res && !err) {
this.refs.toasts.success(
'Data for this encounter has been saved.',
'Records saved!'
);
} else {
this.refs.toasts.error(
'An unknown error has occurred. Reload the page and try again.',
'Error!'
);
}
});
I combine all of my state variables into an object using {}, which in turn becomes dataObj in the method. However, dataObj.impressionData exists, and is an array containing objects, however, data is missing from any of the objects in the array.
For example, dataObj.impressionData[0].diagnosis should be an object, in fact, it is supposed to be an exact copy of an object already pulled from the database. However, if I console.log it, the object is empty.
I have verified that the data exists as it should at each step before passing to the Meteor method. I console.log the object immediately before calling Meteor.call and immediately after calling check in my method. I cannot for the life of me understand why data is missing.
What am I forgetting?
EDIT: I've changed my code so that the data is now added to the state directly from a ref. Now the server method does properly receive the object. However, in the following code:
if (dataObj.impressionData && dataObj.impressionData.length > 0) {
dataObj.impressionData.forEach(obj => {
console.log(obj); // obj.diagnosis exists and is as expected
const x = ICD10Codes.findOne({ _id: obj.diagnosis._id });
console.log(x); // this also works as it should
impressionFields.push({ patientId: appt.patient._id, diagnosis: x, note: obj.note, x });
});
}
Setting diagnosis to x, which I KNOW is a valid copy of the object straight from the database yields the same results:
meteor:PRIMARY> db.EncounterData.findOne()
...
"impression" : {
"patientId" : "47de32b428d8c4aaac284af3",
"appointmentId" : "TwL7DF9FoXPRgmrjR",
"fields" : [
{
"patientId" : "47de32b428d8c4aaac284af3",
"diagnosis" : {
}
}
]
},
...
I think I'm going crazy.
So your issue comes down to the fact that this.setState is an asynchronous function, so when you make your Meteor call, this.state hasn't actually be updated yet. As such, you need to wait for the this.setState call to finish. The only way to do this is to use the React lifecycle methods. You can use either componentWillUpdate (called before the next render) or componentDidUpdate (called after the next render).
var MyComponent = React.createClass({
save: function() {
...
case 'impression':
this.setState({ impressionData: data }, this.callServerMetho‌​d);
break;
...
},
// This is one of the React lifecycle methods
componentWillUpdate: function(nextProps, nextState) {
// Put your Meteor call here
// Make sure to use nextState instead of this.state
// This way you know that this.state has finished updating
}
});
I solved the issue myself - it turns out that I was importing my SimpleSchema objects as default but exporting my SimpleSchema objects as named. The SimpleSchema objects were thus invalid.

Categories