React - Children Accessing Parents state too early? - javascript

When using a callback from a child to app.js, the callback function in app.js sets the state for an element.
In the same child-function, on the next line, the function accesses this state, which is passed along as a prop.
Is there a way to make sure that this next line executes with the updated state already? If not, whats a better way to implement this?
Child Component
function handleCountrySelected(selected) {
let selectedValues = {
countries: selected,
planningGroups: null,
};
onFilterChange(selectedValues); // changes selectedFilters.country via callback
console.log(selectedFilters.countries); // still old state. selectedFilters is a prop from apps.state. Here it is still showing the old value
filters_changed();
autoApply && applyFilter(); // applyFilter fails because its accessing old state, still.
}
App.js
updateSelectedFilters(selectedValues) {
this.setState({
selectedFilters: {
...this.state.selectedFilters,
...selectedValues
}
}
...
<NavbarTopFilters onApplyFilters={this.onApplyFilters}
selectedFilters={this.state.selectedFilters}
onFilterChange={this.updateSelectedFilters}
/>

setState is an async function. You need to pass in the rest of the lines as a callback.
function updateSelectedFilters(selectedValues, callback) {
this.setState({selectedFilters: {...}}, callback);
...
}
onFilterChange(selectedValues, () => { console.log(...) });

Related

React JS Updating item in State object to take effect immediately

React JS class component
I know there have been many posts on this subject but I can't seem to get this scenario to work.
Basically on my HandleClickSave event I want to update an item in my object in state without affecting the other values and then passing this updated oblect onto my service to get updated in the db.
The 'item' in question is the 'design' from the (unLayer) React-email-editor.
Problem is after the service is run in 'HandleClickSave' point 3 below, the receiving field 'DesignStructure' in the db is NULL every time. The other two fields are fine as these are saved to state object elsewhere.
Part of the problem is that the Email-Editor doesn't have an 'onChange' property which is where I would normally update the state. The other two values in the object are input texts and they do have an onChange which is how their state counterparts are updated.
This is the object 'NewsletterDesign':
{
"DesignId": "1",
"DesignName": "DesignLayout 1 Test",
"DesignStructure": null
}
In my React class component...
this.state = {
NewsletterDesign: {}
}
And the HandleClickSave event....
HandleClickSave () {
const { NewsletterDesign } = this.state
this.editor.saveDesign((design) => {
this.setState(prevState => ({
NewsletterDesign: {
...prevState.NewsletterDesign,
DesignStructure: design
}
}));
// Update to database passing in the object 'NewsletterDesign'. Field 'DesignStructure' in db is null every time, but other two fields are updated.
NewsletterService.UpdateCreateNewsletterDesign(NewsletterDesign)
etc....etc..
React's setState is not update immediately. read more here.
You can simply do it inside setState by
this.setState(prevState => {
const newState = {
NewsletterDesign: {
...prevState.NewsletterDesign,
DesignStructure: design
}
};
NewsletterService.UpdateCreateNewsletterDesign(newState.NewsletterDesign);
return newState;
});
The setState is an async operation. Meaning, that it's not guaranteed that the new state that you have updated will be accessible just after the state is updated. You can read more here
So in such cases, one of the way is to do the required operation first and then use the result at multiple places.
HandleClickSave () {
const { NewsletterDesign } = this.state
this.editor.saveDesign((design) => {
let newNewsletterDesign = { ...NewsletterDesign,
DesignStructure: design
};
this.setState(newNewsletterDesign);
NewsletterService.UpdateCreateNewsletterDesign(newNewsletterDesign)

Vuex commit after await won't update state

I've read multiple similar questions about this here and elsewhere, but I can't figure it out.
I have a form with mapGetters and input values that should update based on Vuex state:
...mapGetters({
show: "getShow"
}),
sample form input (I'm using Bootstrap Vue):
<b-form-input
id="runtime"
name="runtime"
type="text"
size="sm"
v-model="show.runtime"
placeholder="Runtime"
></b-form-input>
Then I have this method on the form component:
async searchOnDB() {
var showId = this.show.showId;
if (!showId) {
alert("Please enter a showId");
return;
}
try {
await this.$store.dispatch("searchShowOnDB", showId);
} catch (ex) {
console.log(ex);
alert("error searching on DB");
}
},
and this action on the store:
async searchShowOnDB({ commit, rootState }, showId) {
var response = await SearchAPI.searchShowOnDB(showId);
var show = {
show_start: response.data.data.first_aired,
runtime: response.data.data.runtime,
description: response.data.data.overview
};
//I'm updating the object since it could already contain something
var new_show = Object.assign(rootState.shows.show, show);
commit("setShow", new_show);
}
mutation:
setShow(state, show) {
Vue.set(state, "show", show);
}
searchAPI:
export default {
searchShowOnDB: function (showId) {
return axios.get('/search/?id=' + showId);
},
}
Everything works, the API call is executed, I can even see the Vuex updated state in Vue Devtools, but the form is not updated.
As soon as I write something in an input field or hit commit in Vue Devtools, the form fields show_start, runtime, description all get updated.
Also, this works correctly and updates everything:
async searchShowOnDB({ commit, rootState }, showId) {
var show = {
show_start: "2010-03-12",
runtime: 60,
description: "something"
};
//I'm updating the object since it could already contain something
var new_show = Object.assign(rootState.shows.show, show);
commit("setShow", new_show);
}
I don't know what else to do, I tried by resolving Promises explicitly, remove async/await and use axios.get(...).then(...), moving stuff around... nothing seems to work.
On line 15 of your /modules/search.js you're using Object.assign() on rootState.search.show. This mutates the search prop of the state (which is wrong, btw, you should only mutate inside mutations!). Read below why.
And then you're attempting to trigger the mutation. But, guess what? Vue sees it's the same value, so no component is notified, because there was no change. This is why you should never mutate outside of mutations!
So, instead of assigning the value to the state in your action, just commit the new show (replace lines 15-16 with:
commit('setShow', show);
See it here: https://codesandbox.io/s/sharp-hooks-kplp7?file=/src/modules/search.js
This will completely replace state.show with show. If you only want to merge the response into current state.show (to keep some custom stuff you added to current show), you could spread the contents of state.show and overwrite with contents of show:
commit("setShow", { ...rootState.search.show, ...show });
Also note you don't need Vue.set() in your mutation. You have the state in the first parameter of any mutation and that's the state of the current module. So just assign state.show = show.
And one last note: when your vuex gets bigger, you might want to namespace your modules, to avoid any name clashes.
All props of objects in a state that is used in templates must exist or you should call Vue.set for such properties.
state: {
show: {
runtime: null // <- add this line
}
},
You call Vue.set for the whole object but it already exist in the state and you do not replace it by a new one you just replace props. In your case you have an empty object and add the 'runtime' prop it it using Object.assign.
Also all manipulations with state should be done in mutations:
var new_show = {
runtime: response.data.url
};
commit("setShow", new_show);
...
mutations: {
setShow(state, new_show) {
Object.assign(state.show, new_show)
}
},

setState doesn't update all state properties after call

The thing is, that I use setState to update state.n and state.data. I want to call setState onClick, using handler. After calling setState, I perform a callback console.log to watch the changed state properties, but only state.data is changed, not state.n.
handler(e) {
this.setState((state) => {
let arr = state.data;
arr.push(fakeTableData[state.n+1]);
this.setState({n: state.n+1, data: arr});
}, console.log.bind(null, this.state.n));
}
After one handler used, I expect to see state.n incremented, but I had only state.data changed, not state.n. And I recieve a warning, like this: "Warning: An update (setState, replaceState, or forceUpdate) was scheduled from inside an update function. Update functions should be pure, with zero side-effects. Consider using componentDidUpdate or a callback." I've read a tutorial on the main react site, but there not enough info.
let's try let arr = [...state.data]
You should not use setState inside function that sets state. That is why you got an error. Instead you should return state. Try this:
handler = (e) => {
this.setState((state) => {
let arr = state.data;
arr.push(fakeTableData[state.n+1]);
return {n: state.n+1, data: arr};
}, () => { console.log(this.state.n) });
}

Is there a proper way of resetting a component's initial data in vuejs?

I have a component with a specific set of starting data:
data: function (){
return {
modalBodyDisplay: 'getUserInput', // possible values: 'getUserInput', 'confirmGeocodedValue'
submitButtonText: 'Lookup', // possible values 'Lookup', 'Yes'
addressToConfirm: null,
bestViewedByTheseBounds: null,
location:{
name: null,
address: null,
position: null
}
}
This is data for a modal window, so when it shows I want it to start with this data. If the user cancels from the window I want to reset all of the data to this.
I know I can create a method to reset the data and just manually set all of the data properties back to their original:
reset: function (){
this.modalBodyDisplay = 'getUserInput';
this.submitButtonText = 'Lookup';
this.addressToConfirm = null;
this.bestViewedByTheseBounds = null;
this.location = {
name: null,
address: null,
position: null
};
}
But this seems really sloppy. It means that if I ever make a change to the component's data properties I'll need to make sure I remember to update the reset method's structure. That's not absolutely horrible since it's a small modular component, but it makes the optimization portion of my brain scream.
The solution that I thought would work would be to grab the initial data properties in a ready method and then use that saved data to reset the components:
data: function (){
return {
modalBodyDisplay: 'getUserInput',
submitButtonText: 'Lookup',
addressToConfirm: null,
bestViewedByTheseBounds: null,
location:{
name: null,
address: null,
position: null
},
// new property for holding the initial component configuration
initialDataConfiguration: null
}
},
ready: function (){
// grabbing this here so that we can reset the data when we close the window.
this.initialDataConfiguration = this.$data;
},
methods:{
resetWindow: function (){
// set the data for the component back to the original configuration
this.$data = this.initialDataConfiguration;
}
}
But the initialDataConfiguration object is changing along with the data (which makes sense because in the read method our initialDataConfiguration is getting the scope of the data function.
Is there a way of grabbing the initial configuration data without inheriting the scope?
Am I overthinking this and there's a better/easier way of doing this?
Is hardcoding the initial data the only option?
extract the initial data into a function outside of the component
use that function to set the initial data in the component
re-use that function to reset the state when needed.
// outside of the component:
function initialState (){
return {
modalBodyDisplay: 'getUserInput',
submitButtonText: 'Lookup',
addressToConfirm: null,
bestViewedByTheseBounds: null,
location:{
name: null,
address: null,
position: null
}
}
}
//inside of the component:
data: function (){
return initialState();
}
methods:{
resetWindow: function (){
Object.assign(this.$data, initialState());
}
}
Caution, Object.assign(this.$data, this.$options.data()) does not
bind the context into data().
So use this:
Object.assign(this.$data, this.$options.data.apply(this))
cc this answer was originally here
To reset component data in a current component instance you can try this:
Object.assign(this.$data, this.$options.data())
Privately I have abstract modal component which utilizes slots to fill various parts of the dialog. When customized modal wraps that abstract modal the data referred in slots belongs to parent
component scope. Here is option of the abstract modal which resets data every time the customized modal is shown (ES2015 code):
watch: {
show (value) { // this is prop's watch
if(value) {
Object.assign(this.$parent.$data, this.$parent.$options.data())
}
}
}
You can fine tune your modal implementation of course - above may be also executed in some cancel hook.
Bear in mind that mutation of $parent options from child is not recommended, however I think it may be justified if parent component is just customizing the abstract modal and nothing more.
If you are annoyed by the warnings, this is a different method:
const initialData = () => ({})
export default {
data() {
return initialData();
},
methods: {
resetData(){
const data = initialData()
Object.keys(data).forEach(k => this[k] = data[k])
}
}
}
No need to mess with $data.
I had to reset the data to original state inside of a child component, this is what worked for me:
Parent component, calling child component's method:
<button #click="$refs.childComponent.clearAllData()">Clear All</button >
<child-component ref='childComponent></child-component>
Child component:
defining data in an outside function,
referencing data object by the defined function
defining the clearallData() method that is to be called upon by the
parent component
function initialState() {
return {
someDataParameters : '',
someMoreDataParameters: ''
}
}
export default {
data() {
return initialState();
},
methods: {
clearAllData() {
Object.assign(this.$data, initialState());
},
There are three ways to reset component state:
Define key attribute and change that
Define v-if attribute and switch it to false to unmount the component from DOM and then after nextTick switch it back to true
Reference internal method of component that will do the reset
Personally, I think the first option is the clearest one because you control the component only via Props in a declarative way. You can use destroyed hook to detect when the component got unmount and clear anything you need to.
The only advance of third approach is, you can do a partial state reset, where your method only resets some parts of the state but preserves others.
Here is an example with all the options and how to use them:
https://jsbin.com/yutejoreki/edit?html,js,output

this.state.foo is different in _onChange and different in render()?

In _onChange method, I wait for a change in this.state.name. On the change _updateArticle method is called:
_onChange() {
var team = this.getTeamState();
this.setState({name: team});
console.log(this.state.name); //"Boston Celtics" //this should have changed but it didn't
this.unbind("articles");
this._updateArticle();
},
then in _updateArticle a new reference is created using this new state.
_updateArticle(team) {
var teamRef = new Firebase(this.props.baseUrl + team + "/results");
console.log(this.state.team); // "Boston Celtics" //this should have been new but its not
this.bindAsArray(teamRef, 'articles');
},
In render method however just to check the state I put console.log to check. In render, this.state.name has been updated properly. My question is why is this.state different outside of render function?.
As per the API docs:
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.
If you really want to access state right after calling setState, use the callback parameter in the setState method:
setState(function|object nextState[, function callback])
So your code should look like:
_onChange() {
var team = this.getTeamState();
this.setState({name: team}, function() {
console.log(this.state.name);
this.unbind("articles");
this._updateArticle(this.state.team);
});
}
Hope this helps.

Categories

Resources