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

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"}

Related

Vue Reactivity: Why replacing an object's property (array) does not trigger update

I have a Vue app where I'm trying to make a thin wrapper over the Mapbox API. I have a component which has some simple geojson data, and when that data is updated I want to call a render function on the map to update the map with that new data. A Vue watcher should be able to accomplish this. However, my watcher isn't called when the data changes and I suspect that this is one of the cases that vue reactivity can't catch. I'm aware that I can easily fix this problem using this.$set, but I'm curious as to why this isn't a reactive update, even though according to my understanding of the rules it should be. Here's the relevant data model:
data() {
return{
activeDestinationData: {
type: "FeatureCollection",
features: []
}
}
}
Then I have a watcher:
watch: {
activeDestinationData(newValue) {
console.log("Active destination updated");
if (this.map && this.map.getSource("activeDestinations")) {
this.map.getSource("activeDestinations").setData(newValue);
}
},
}
Finally, down in my app logic, I update the features on the activeDestination by completely reassigning the array to a new array with one item:
// Feature is a previously declared single feature
this.activeDestinationData.features = [feature];
For some reason the watcher is never called. I read about some of the reactivity "gotchas" here but neither of the two cases apply here:
Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
What am I missing here that's causing the reactivity to not occur? And is my only option for intended behavior this.set() or is there a more elegant solution?
as default vue will do a shallow compare, and since you are mutating the array rather than replacing, its reference value is the same. you need to pass a new array reference when updating its content, or pass the option deep: true to look into nested values changes as:
watch: {
activeDestinationData: {
handler(newValue) {
console.log("Active destination updated");
if (this.map && this.map.getSource("activeDestinations")) {
this.map.getSource("activeDestinations").setData(newValue);
}
},
deep: true
}
}
If you need to watch a deep structure, you must write some params
watch: {
activeDestinationData: {
deep: true,
handler() { /* code... */ }
}
You can read more there -> https://v2.vuejs.org/v2/api/#watch
I hope I helped you :)

Best practices for mapStateToProps value assignment

I am right now working on a project in which there are quite some teams involved, and there is a little mess around where the variables are initialized and communicated to the store.
It seems like the store is mainly looking like the following.
const initialState = {
myStoreData: null
}
export default (state) => {
...
return state
}
Then later in the component the team is writing things like the following. In which they reference the variable that was poorly initialized in the store, and they are setting there the value for the prop.
function mapStateToProps(state, ownProps){
return {
someValue: state.myStoreData.someValue || '',
someOtherProperty: state.myStoreData.someOtherProperty || '',
anotherProperty: state.anotherProperty || false, // NOTE: This one doesn't exist in the store for example
};
Is there some kind of baked article that shows best practices regarding where would be the best place to keep the initial state of the application, and whether doing this kind of assignments are calling for bugs, or just simple ways to not have to modify the store initialState each time?
For me it seems like calling fro trouble, but still, I couldn't find the article backing me up
It's good to have the initialState on the reducer, so you know exactly what properties and initial values are to put in your state.
For an advanced usage of using mapStateToProps, you can check this article, React, Reselect and Redux. It uses reselect which is very performant, efficient and composable. It is very suitable for large applications with a lot of state in redux.
Hope this helps.
I'm not sure I agree with the correlation between the initial state of your store and the default value of what a UI expects, to me they are two separate problems.
For example, myStoreData.someValue could be represented as null in the store because it's never been set, but when rendering the UI null maybe doesn't make sense e.g. if you are binding that field to an input, therefore you want to swap null for '' or even a preset 'YES' / 'NO' etc. but just for the purpose of rendering - I personally don't see why this should be dictated by your data model.
In mapStateToProps you are effectively creating a view model, so it's the point where you prepare data for the view, so I don't see anything wrong with the code in your question (apart from the lack of selector use, but that's a different topic). If you simply don't like the idea of it being done at that point then you could move the logic completely into the view which keeps it self-contained, view specific, and makes it a clear intention that the incoming data should come straight from the store and any fix-ups happen at view level e.g.
class MyComponent extends React.Component {
// let the view set the expected default values for the missing props
defaultProps: {
someProperty: '',
someOtherProperty: '',
anotherProperty: false
}
}
...
function mapStateToProps(state, ownProps){
// build a view model based on the properties that you expect,
// don't include props that don't have a value or not set
const vm = {};
const { myStoreData } = state;
if (typeof myStoreData.someValue === 'string') {
vm.someValue = myStoreData.someValue;
}
if (typeof myStore.someOtherProperty === 'string') {
vm.someOtherProperty = myStoreData.someOtherProperty;
}
if (typeof myStore.anotherProperty === 'boolean') {
vm.anotherProperty = myStore.anotherProperty;
}
return vm;
}
You could leverage a validation lib like validator for simplicity, but you get the idea.

What is the proper way to have a copy of redux state on a component prop

I have something like this:
ngOnInit() {
this.reduxServiceHandle = this.reduxService.subscribe(() =>
this.reduxUpdates()
);
}
reduxUpdates(): void {
const newState: TransportationControlTowerState = this.reduxService.getState();
// Check if feature groups object has changed
if (!isEqual(this.groups, newState.groups)) {
...
this.groups = newState.groups;
}
}
This is my reducer:
case TransportationControlTowerActions.ADD_GROUP:
return newState = {
...state,
groups: { ...state.groups, [payload.groupId]: payload.group }
};
break;
So, my question is: Do I need to clone deep the state before save it on this.groups prop? this.groups = newState.groups;
I think that every time I change the redux state, I return a new state object so there won't be a problem with my local state(this.groups) pointing to the old state.
But I just want to make sure I am not following any anti pattern.
Regards,
Official Redux docs says:
State is read-only
The only way to change the state is to emit an action, an object describing what happened.
This ensures that neither the views nor the network callbacks will ever write directly to the state. Instead, they express an intent to transform the state.
You can view full list of core principles here https://redux.js.org/introduction/three-principles

MobX store doesn't get updated in react

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.

What is the right/preferred way to make a "Edit Detail" component in React?

I'm working on a page whose 'Data Model' is a collection, for example, an array of people. They are packed into React Components and tiled on the page. Essentially it's like:
class App extends React.Component {
constructor() {
super();
this.state = { people: /* some data */ };
}
render () {
return (
<div>
{this.state.people.map((person) =>
<People data={person}></People>)}
</div>);
}
}
Now I want to attach an edit section for each entry in <People> component, which allows the user to update the name, age ... all kinds of information for a specific entry.
Since React does not support mutating props inside components, I searched and found that adding callbacks as props can solve the problem of passing data to parent. But since there are many fields to update, there would be many callbacks such as onNameChanged, onEmailChanged... which could be very ugly (also more and more verbose as the number of fields keeps growing).
So what is the right way for it?
Honestly? The best way is Flux (back to that in a minute).
If you start to get into the process of passing data down the tree in the form of props, then passing it back up to be edited using callbacks, then you're breaking the unidirectional data flow that React is built around.
However, not all projects need to be written to ideal standards and it is possible to build this without Flux (and sometimes it might even be the right solution).
Without Flux
You can implement this without the need for a mass of callbacks, by passing down a single edit function as a prop. This function should take an id and a new person object, then update the state inside the parent component whenever it runs. Here's an example.
editPerson(id, editedPerson) {
const people = this.state.people;
const newFragment = { [id]: editedPerson };
// create a new list of people, with the updated person in
this.setState({
people: Object.assign([], people, newFragment)
});
},
render() {
// ...
{this.state.people.map((person, index) => {
const edit = this.editPerson.bind(this, index);
return (
<People data={person} edit={edit}></People>
);
})}
// ...
}
Then inside your person component, any time you make a change to the person, simply pass the person back up to the parent state with the callback.
However, if you visualize the flow of data through your application, you've now created a cycle that looks something like this.
App
^
|
v
Person
It's no longer trivial to work out where the data in app came from (it is still quite simple in such a small app, but obviously the bigger it gets the harder it is to tell.
With Flux
In the beginning, Facebook developers wrote React applications with unidirectional data flows and they saw that it was good. However, a need arose for data to go up the tree, which resulted in a crisis. How shall our data flow be unidirectional and still return to the top of the tree? And on the seventh day, they created Flux(1) and saw that it was very good.
Flux allows you to describe your changes as actions and pass them out of your components, to stores (self contained state boxes) which understand how to manipulate their state based on the action. Then the store tells all the components that care about it that something has changed, at which point the components can fetch new data to render.
You regain your unidirectional data flow, with an architecture that looks like this.
App <---- [Stores]
| ^
v |
Person --> Dispatcher
Stores
Rather than keeping your state in your <App /> component, you would probably want to create a People store to keep track of your list of people.
Maybe it would look something like this.
// stores/people-store.js
const people = [];
export function getPeople() {
return people;
}
function editPerson(id, person) {
// ...
}
function addPerson(person) {
// ...
}
function removePerson(id) {
// ...
}
Now, we could export these functions and let our components call them directly, but that's bad because it means that our components have to have knowledge of the design of the store and we want to keep them as dumb as possible.
Actions
Instead, our components create simple, serializable actions that our stores can understand. Here are some examples:
// remove person with id 53
{ type: 'PEOPLE_REMOVE', payload: 53 }
// create a new person called John Foo
{ type: 'PEOPLE_ADD', payload: { name: 'John Foo' } }
// edit person 13
{
type: 'PEOPLE_EDIT',
payload: {
id: 13,
person: { name: 'Unlucky Bill' }
}
}
These actions don't have to have these specific keys, they don't even have to be objects either, this is just the convention from Flux Standard Actions.
Dispatcher
Now, we have tell our store how to deal with these actions when they arrive.
// stores/people-store.js
// ...
dispatcher.register(function(action) {
switch(action.type) {
case 'PEOPLE_REMOVE':
removePerson(action.payload);
case 'PEOPLE_ADD':
addPerson(action.payload);
case 'PEOPLE_EDIT':
editPerson(action.payload.id, action.payload.person);
}
});
Phew. Lot of work so far, nearly there.
Now we can start to dispatch these actions from our components.
// components/people.js
// ...
onEdit(editedPerson) {
dispatcher.dispatch({
type: 'PEOPLE_EDIT',
payload: {
id: this.props.id,
person: editedPerson
}
});
}
onRemove() {
dispatcher.dispatch({
type: 'PEOPLE_REMOVE',
payload: this.props.id
});
}
// ...
When you edit the person, call the this.onEdit method and it will dispatch the appropriate action to your stores. Same goes for removing a person. Normally you'd move this stuff into action creators, but that's a topic for another time.
Ok, finally getting somewhere! Now our components can create actions that update the data in our stores. How do we get that data back into our components?
Initially, it's very simple. We can require the store in our top level component and simply ask for the data.
// components/app.js
import { getPeople } from './stores/people-store';
// ...
constructor() {
super();
this.state = { people: getPeople() };
}
We can pass this data down in exactly the same way, but what happens when the data changes?
The official stance from Flux is basically "Not our problem". Their examples use Node's Event Emitter class to allow stores to accept callback functions that are called when the store updates.
This allows you to write code that looks something like this:
componentWillMount() {
peopleStore.addListener(this.peopleUpdated);
},
componentWillUnmount() {
peopleStore.removeListener(this.peopleUpdated);
},
peopleUpdated() {
this.setState({ people: getPeople() });
}
Really, the ball is in your court on this one. There are many other strategies for getting the data back into your program. Reflux creates the listen method for you automatically, Redux allows you to declaratively specify which components receive which parts of the store as props, then it handles the updating. Spend enough time with Flux and you'll find a preference.
Now, you're probably thinking, blimey — this seems like a lot of effort to go to just to add edit functionality to a component; and you're right, it is!
For small applications, you probably don't need Flux.
Sure there are lots of benefits, but the additional complexity just isn't always warranted. As your application grows, you'll find that if you've fluxed it up, it will be much easier to manage, maintain and debug.
The trick is to know when it's appropriate to use the Flux architecture and hopefully when the time comes, this overly long, rambling answer will have cleared things up for you.
This isn't actually true.

Categories

Resources