MobX store doesn't get updated in react - javascript

What i want to achieve
I have a store object and i want to populate one of it's properties (which is an empty array) with instances of another object. And i want one of my react component automatically updated when part of the mentioned array instance is changed.
What my problem is
By logging out the value of this constructed store object i can see the change in the browser console, but it doesn't get updated automatically in the react component when its value changed.
So i'd like to get hints, examples of how to implement a solution like this in general.
Details
My project
I want to create a MobX store called Session which would store everything my react webapp needs.
This webapp would be a document handling tool, so after creating new or loading existing documents i want to add the Document object to the Session (into an object array called documents).
Some more details: a Document consists of one or more section. So using a WYSIWYG editor i add its content to the given section every time it's content changes.
Problem
I can add a new Document to the Session, i can update section as well(I can log out the value of a section in console), but using the Session reference to this document and section in a react component it doesnt update its state when section value is changed.
To my understanding in my example the reference of a Document is not changed when the value of a section is changed and hence it doesn't trigger MobX to react.
What i found so far
I started to dig in the deep, dark web and found this article.
So i started getting excited since asStructure (or asMap) seemed to solve this issue, but it looks like asStructure in MobX is deprecated.
Then i found this issue thread, where a thing called observable.structurallyCompare is mentioned. But again i found nothing about this in MobX documentation so im puzzled how to implement it.
So im stuck again and have no idea how to solve this problem.
Code excerpts from my project
This is how i reference to the mentioned Session value in the main react component:
import userSession from '../client/Session';
import {observer} from 'mobx-react';
#observer class App extends React.Component {
...
render() {
return (
...
<div>{JSON.stringify(userSession.documents[0].content.sections)}</div>
...
This is how i update the section in the editor react component:
import userSession from '../../client/Session';
...
handleChange(value,arg2,arg3,arg4) {
this.setState({ content: value, delta: arg4.getHTML()});
userSession.documents[0].updateSectionContent(this.props.id, this.state.delta);
}
}
...
Session.js excerpt:
class Session {
constructor(){
extendObservable(this, {
user: {
},
layout: {
},
documents: []
})
//current hack to add an empty Document to Session
this.documents.push(new Document());
}
//yadayadayada...
#action addNewSection() {
userSession.documents[0].content.sections.push({
type: "editor",
id: userSession.documents[0].getNextSectionID(),
editable: true,
content: "",
placeholder: "type here to add new section..."
});
}
}
var userSession = window.userSession = new Session();
export default userSession;
Document.js
import {extendObservable, computed, action} from "mobx";
import userSession from "./Session";
class Document {
constructor(doc = null) {
if (doc == null) {
console.log("doc - no init value provided");
extendObservable(this,{
labels: {
title: "title",
description: "description",
owners: ["guest"]
},
content: {
sections: [
{
type: "editor",
id: "sec1",
editable: true,
placeholder: "this is where the magic happens"
},
]
}
})
} else {
console.log("doc - init values provided: ");
extendObservable(this,{
labels: doc.labels,
content: doc.content
});
}
}
getNextSectionID(){
return `sec${this.content.sections.length}`;
}
#action updateSectionContent(sectionID, delta) {
console.log("doc - looking for section " + sectionID + " to update with this: " + delta);
let index = this.content.sections.findIndex(
section => section.id === sectionID
);
userSession.documents[0].content.sections[index].content = delta;
}
}
export default Document;
Ps.: atm moment i don't remember why i made Document properties observable, but for some reason it was necessary.

Unfortunately, you are implementing mobx with React the incorrect way. To make it more comprehensible, I suggest you look into the implementation of the HOC observer that mobx-react provide.
Basically, what this HOC does is to wrap your component inside another React component that implement shouldComponentUpdate that check when the props referred inside render function change, and trigger the re-render. To make React component reactive to change in mobx store, you need to pass the store data to them as props, either in React traditional way, or via the inject HOC that mobx-react provide.
The problem while your code does not react to change in the store is because you import the store and use them directly inside your component, mobx-react cannot detect change that way.
To make it reactive, instead of import it and use it directly in your App component, you can pass the store as a prop as follow:
#observer
class App extends React.Component {
...
render() {
return (
...
<div>{this.props.sections}</div>
...);
}
}
Then pass the store to App when it's used:
<App sections={userSession.documents[0].content.sections} />
You can also have a look at how to use inject here: https://github.com/mobxjs/mobx-react.
An just a suggestion: before jumping directly on some best pattern, try to stick with the basic, standard way that library author recommend and understand how it works first, you can consider other option after you got the core idea.

Related

NextJS - can't identify where state change is occurring on page component when using shallow Router push

I have an app. that uses NextJS. I have a page that looks like the following:
import React from 'react'
import { parseQuery } from '../lib/searchQuery'
import Search from '../components/search'
class SearchPage extends React.Component {
static getInitialProps ({ query, ...rest }) {
console.log('GET INITIAL PROPS')
const parsedQuery = parseQuery(query)
return { parsedQuery }
}
constructor (props) {
console.log('CONSTRUCTOR OF PAGE CALLED')
super(props)
this.state = props.parsedQuery
}
render () {
return (
<div>
<div>
<h1>Search Results</h1>
</div>
<div>
<h1>DEBUG</h1>
<h2>PROPS</h2>
{JSON.stringify(this.props)}
<h2>STATE</h2>
{JSON.stringify(this.state)}
</div>
<div>
<Search query={this.state} />
</div>
</div>
)
}
}
export default SearchPage
getInitialProps is ran for SSR - it receives the query string as an object (via Express on the back end) runs it through a simple 'cleaner' function - parseQuery - which I made, and injects it into the page via props as props.parsedQuery as you can see above. This all works as expected.
The Search component is a form with numerous fields, most of which are select based with pre-defined fields and a few a number based input fields, for the sake of brevity I've omitted the mark up for the whole component. Search takes the query props and assigns them to its internal state via the constructor function.
On changing both select and input fields on the Search component this code is ran:
this.setState(
{
[label]: labelValue
},
() => {
if (!this.props.homePage) {
const redirectObj = {
pathname: `/search`,
query: queryStringWithoutEmpty({
...this.state,
page: 1
})
}
// Router.push(href, as, { shallow: true }) // from docs.
this.props.router.push(redirectObj, redirectObj, { shallow: true })
}
}
)
The intention here is that CSR takes over - hence the shallow router.push. The page URL changes but getInitialProps shouldn't fire again, and subsequent query changes are handled via componentWillUpdate etc.. I confirmed getInitialProps doesn't fire again by lack of respective console.log firing.
Problem
However, on checking/unchecking the select fields on the Search component I was surprised to find the state of SearchPage was still updating, despite no evidence of this.setState() being called.
constructor isn't being called, nor is getInitialProps, so I'm unaware what is causing state to change.
After initial SSR the debug block looks like this:
// PROPS
{
"parsedQuery": {
"manufacturer": [],
"lowPrice": "",
"highPrice": ""
}
}
// STATE
{
"manufacturer": [],
"lowPrice": "",
"highPrice": ""
}
Then after checking a select field in Search surprisingly it updates to this:
// PROPS
{
"parsedQuery": {
"manufacturer": ["Apple"],
"lowPrice": "",
"highPrice": ""
}
}
// STATE
{
"manufacturer": ["Apple"],
"lowPrice": "",
"highPrice": ""
}
I can't find an explanation to this behaviour, nothing is output to the console and I can't find out how to track state changes origins via dev. tools.
Surely the state should only update if I were to do so via componentDidUpdate? And really shouldn't the parsedQuery prop only ever be updated by getInitialProps? As that's what created and injected it?
To add further confusion, if I change a number input field on Search (such as lowPrice), the URL updates as expected, but props nor page state changes in the debug block. Can't understand this inconsistent behaviour.
What's going on here?
EDIT
I've added a repo. which reproduces this problem on as a MWE on GitHub, you can clone it here: problem MWE repo.
Wow, interesting problem. This was a fun little puzzle to tackle.
TL;DR: This was your fault, but how you did it is really subtle. First things first, the problem is on this line:
https://github.com/benlester/next-problem-example/blob/master/frontend/components/search.js#L17
Here in this example, it is this:
this.state = props.parsedQuery
Let's consider what is actually happening there.
In IndexPage.getInitialProps you are doing the following:`
const initialQuery = parseQuery({ ...query })
return { initialQuery }
Through Next's mechanisms, this data passes through App.getInitialProps to be returned as pageProps.initialQuery, which then becomes props.initialQuery in IndexPage, and which is then being passed wholesale through to your Search component - where your Search component then "makes a copy" of the object to avoid mutating it. All good, right?
You missed something.
In lib/searchQuery.js is this line:
searchQuery[field] = []
That same array is being passed down into Search - except you aren't copying it. You are copying props.query - which contains a reference to that array.
Then, in your Search component you do this when you change the checkbox:
const labelValue = this.state[label]
https://github.com/benlester/next-problem-example/blob/master/frontend/components/search.js#L57
You're mutating the array you "copied" in the constructor. You are mutating your state directly! THIS is why initialQuery appears to update on the home page - you mutated the manufacturers array referenced by initialQuery - it was never copied. You have the original reference that was created in getInitialProps!
One thing you should know is that even though getInitialProps is not called on shallow pushes, the App component still re-renders. It must in order to reflect the route change to consuming components. When you are mutating that array in memory, your re-render reflects the change. You are NOT mutating the initialQuery object when you add the price.
The solution to all this is simple. In your Search component constructor, you need a deep copy of the query:
this.state = { ...cloneDeep(props.query) }
Making that change, and the issue disappears and you no longer see initialQuery changing in the printout - as you would expect.
You will ALSO want to change this, which is directly accessing the array in your state:
const labelValue = this.state[label]
to this:
const labelValue = [...this.state[label]]
In order to copy the array before you change it. You obscure that problem by immediately calling setState, but you are in fact mutating your component state directly which will lead to all kinds of weird bugs (like this one).
This one arose because you had a global array being mutated inside your component state, so all those mutations were being reflected in various places.

How to import Arrays from my Vue Components to store.js (State management with Vuex)

Good evening guys. :)
I've got a small Problem.
I simply have created an Array in a Stocks Vue Component which I made and now I want to Import the Array in my store.js file where I centralized all the necessary data (Vuex).
If there are any more Questions, just ask me. ^^
where i centralized all the neccessary data (Vuex).
If you want the array in your store why don't you initialize it there instead of your Stocks.vue component? I can't tell much about your app with just a .png. Is there a specific reason you cannot start with the array in the store?
If you had to leave it this way you could set a null value in your store. Something like:
state: {
funds: 10000,
stocks: null
}
Then write a mutation:
mutations: {
SET_STOCKS (state, payload) {
state.stocks = payload;
}
}
Then you could commit the mutation in your component where the payload would be the value of your array. You can get around using mutations by manipulating the state object directly, but it is not recommended.
Nathan's suggestion is a right one (and the Vue way to approach this). One small thing to consider though - what's the benefit of moving that data to your state? It adds complexity and unless other components also need access to it (and you can't pass it down via props), there's no real reason to move data out from a component-level. Not all app's state/data has to be centralized in your state-management system (vuex in this case).
If, however, the data should be globally (app-wide) accessible, then you should declare the stocks array already state-side and in your component Stocks.vue simply map the state:
// store.js
export const store = new Vuex.store({
state: {
funds: 10000,
stocks: [
// Stocks data goes here
]
},
// ...
})
And then map it in your component:
// Stocks.vue
<script>
import { mapState } from 'vuex'
export default {
name: 'stocks',
computed: {
...mapState({
stocks: state => state.stocks
})
}
}
</script>
You can now access stocks in Stocks.vue in your template block or this.stocks in your script block (e.g. under a method).
PS! Welcome to StackOverflow : )
Have you checked the documentation?
The basic idea you need to import the store from the component in which you want to read the store.

Where to put global helper js functions

I have several helper functions I intend to use throughout my Vue.js 2 application (i.e. within components and vuex). The one I'm using as an example here, toCurrency, converts a number to a currency string. I'm wondering where the best place to put this helper function is. I'm currently putting it at the top of my store.js file (I'm using vuex obviously) and calling it throughout the file. However, this means I also need to define it as a method when I want to use it in components. Is there a better place to put them so I don't have to keep defining it or is this the best place?
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
function toCurrency(num){
return "$" + num.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
}
export default new Vuex.Store({
state: {
price: 2
},
getters: {
numString: state => {
return toCurrency(state.funds)
}
},
mutations: {
decrementOne: state => {
state.num --;
alert("Price decremented to " + toCurrency(state.funds))
}
}
})
This sounds like a perfect case for Instance Variables. Essentially I would firstly setup an address such as this.$helperfunctions and then proceed to add all of my methods directly onto this instance member after which they will become available to the rest of your application.
import myObjectFullOfMethods from '../somewhere/foo/bar.js';
new Vue({
beforeCreate: function() {
this.$helperfunctions = new myObjectFullOfMethods();
}
})

React + Flux. Optimised proceedure for passing store state to component

I'm having a little conceptual difficulty with a certain aspect of the React/Flux architecture, I know, crazy, right. It has to do with how a Container should pass the Store to a Component, and how the Component should read from the Store, which as far as I see are interdependent.
As an example - I have a simple chart which updates the x and y range depending on changes to a form.
I have a simple Store, updated from Dispatch events, "XRANGE_CHANGE" and "YRANGE_CHANGE", of an Action.
import Immutable from "immutable";
import { ReduceStore } from "flux/utils";
import Dispatcher from "../Dispatch";
class ChartStore extends ReduceStore {
constructor() {
super(Dispatcher);
}
getInitialState() {
return Immutable.OrderedMap({
xRange: [],
yRange: []
});
}
reduce(state, action) {
switch(action.type) {
case "XRANGE_CHANGE":
return state.set("xRange", action.item);
case "YRANGE_CHANGE":
return state.set("yRange", action.item);
default:
console.error("Action type not found");
return state;
}
}
}
export default new ChartStore();
And a Container, which will pass this Store to the Chart component;
import React from "react";
import { Container } from "flux/utils";
import ChartAction from "./ChartAction";
import ChartStore from "./ChartStore";
import Chart from "./Component";
class ExampleContainer extends React.Component {
static calculateState() {
const chartStore = ChartStore.getState(),
xRange = chartStore.get("xRange"),
yRange = chartStore.get("yRange")
return {
xRange: xRange,
yRange: yRange,
xChange: ChartAction.xChange,
yChange: ChartAction.yChange
};
}
static getStores() {
return [ChartStore];
}
render() {
const state = this.state;
return <Chart
// actions
xChange={state.xChange}
yChange={state.yChange}
// !!!!! here's where my confusion lies !!!!!
//store={state.chartStore}
// ammended
xRange={state.xRange}
yRange={state.yRange}
/>
</div>;
}
}
export default Container.create(FinanceContainer);
The commented exclamation marks above indicate where I lose track of the "accepted" React way of doing things.I'm not quite sure of the best way to pass the Store to the Chart component, which will dictate how I read the Store within the component. I have a few options as far as I see, all work but could be completely wrong.
As above, I pass the entire store to the Component and in the Components' render function read store.get("xRange") or store.get("yRange")
In the Container I define xRange={chartStore.get("xRange")} etc.
In Either the Container or the Component I perform store.toJSON()/toObject() and read directly from the result.
I could be completely way off the mark with any of these scenarios. Or any of these ways could be fine.
Any advice would be appreciated. As I continue on I'd like to know I'm carrying out a sensible procedure. Thanks in advance.
As you've noticed, this isn't something with a definitive answer. But I think a good way of determining the "right" methodology is by looking at libraries that are written "for flux" and how they handle these problems. In particular, I would take a look at Redux (a flux implementation) and Reselect (an extension of Redux that addresses this issue further).
The pattern that these libraries use is essentially that your calculateState method ought to transform the flux state into the relevant state information for that container. It should grab relevant information (e.g. state.get('xRange')) as well as possibly performing tranformations on the data held in state if helpful (e.g. range: {x: state.get('xRange'), y: state.get('yRange')}).
As with most things in the flux pattern, the idea here is to provide a definitive "source of truth". You want every sub-component to interpret the flux state in the same way, and you want to have a single method to modify, should the data need to be computed differently. By doing an ETL of the flux state into the container, you achieve that. Should there be some future change to which piece of the flux state is needed for this section of your app, you would merely need to modify this calculateState method, as opposed to all lower usages of that data.

How to update Vue component property when Vuex store state changes?

I'm building a simple presentation tool where I can create presentations, name them and add/remove slides with Vue js and Vuex to handle the app state. All is going great but now I'm trying to implement a feature that detects changes in the presentation (title changed or slide added/removed) and couldn't not yet find the right solution for it. I'll give the example only concerning the title change for the sake of simplicity. Right now in my Vuex store I have:
const state = {
presentations: handover.presentations, //array of objects that comes from the DB
currentPresentation: handover.presentations[0]
}
In my Presentation component I have:
export default {
template: '#presentation',
props: ['presentation'],
data: () => {
return {
shadowPresentation: ''
}
},
computed: {
isSelected () {
if (this.getSelectedPresentation !== null) {
return this.presentation === this.getSelectedPresentation
}
return false
},
hasChanged () {
if (this.shadowPresentation.title !== this.presentation.title) {
return true
}
return false
},
...mapGetters(['getSelectedPresentation'])
},
methods: mapActions({
selectPresentation: 'selectPresentation'
}),
created () {
const self = this
self.shadowPresentation = {
title: self.presentation.title,
slides: []
}
self.presentation.slides.forEach(item => {
self.shadowPresentation.slides.push(item)
})
}
}
What I've done so far is to create a shadow copy of my presentation when the component is created and then by the way of a computed property compare the properties that I'm interested in (in this case the title) and return true if anything is different. This works for detecting the changes but what I want to do is to be able to update the shadow presentation when the presentation is saved and so far I've failed to do it. Since the savePresentation action triggered in another component and I don't really know how pick the 'save' event inside presentation component I fail to update my shadow presentation. Any thoughts on how I could implement such feature? Any help would be very appreciated! Thanks in advance!
I ended up solving this problem in a different way than what I asked in the question but it may be of interest for some. So here it goes:
First I abdicated from having my vue store communicating an event to a component since when you use vuex you should have all your app state managed by the vuex store. What I did was to change the presentation object structure from
{
title: 'title',
slides: []
}
to something a little more complex, like this
{
states: [{
hash: md5(JSON.stringify(presentation)),
content: presentation
}],
statesAhead: [],
lastSaved: md5(JSON.stringify(presentation))
}
where presentation is the simple presentation object that I had at first. Now my new presentation object has a prop states where I will put all my presentation states and each of this states has an hash generated by the stringified simple presentation object and the actual simple presentation object. Like this I will for every change in the presention generate a new state with a different hash and then I can compare my current state hash with the last one that was saved. Whenever I save the presentation I update the lastSaved prop to the current state hash. With this structure I could simple implement undo/redo features just by unshifting/shifting states from states to statesAhead and vice-versa and that's even more than what I intended at first and in the end I kept all my state managed by the vuex store instead of fragmenting my state management and polluting components.
I hope it wasn't too much confusing and that someone finds this helpful.
Cheers
I had this issue when trying to add new properties to my user state so I ended up with this and it works well.
Action in Vuex store
updateUser (state, newObj) {
if (!state.user) {
state.user = {}
}
for (var propertyName in newObj) {
if (newObj.hasOwnProperty(propertyName)) {
//updates store state
Vue.set(state.user, propertyName, newObj[propertyName])
}
}
}
Implementation
Call your store action above from the Vue component
this.updateUser({emailVerified: true})
Object
{"user":{"emailVerified":true},"version":"1.0.0"}

Categories

Resources