Disclaimer: sorry this is going to be a bit long
So I'm working on an intermediately complex app using Vue JS and Vuex.
This is my first Vue SPA project so I'm facing some difficulties with architectural choices, particularity with the question "who should know about the store?"
I'll describe my problem with a dummy example:
Say we have a Post component that has 3 states to get switched with a button:
Read: the component shows the title and text.
Update: the component shows title input and text input
Create: the same component is used for creating a new post, so it's just like update but with empty values.
First Approach: the component handles the store data:
Now in order to show and update a post, the component gets an id prop and selects the post object from a list of posts within the store, and dispatches actions when necessary. It has an update internal attribute to switch between show and update states.
As for the create state, a null id is passed to the component, so it won't fetch anything from the store and shows inputs, it dispatches insert actions.
Example Code
const KnowledgebalePost = {
name: 'Post',
props: ['id'],
data() {
return {
post: {
title: '',
text: '',
},
state: 'show'
}
},
methods: {
updateClicked() {
this.state = 'update';
},
saveClicked() {
this.state = 'show';
const postObject = { id: this.id, ...this.post };
const action = this.id ? 'updatePost' : 'addPost';
this.$store.dispatch(action, postObject);
},
},
created() {
if(this.id) {
// just to simplify
this.post = this.$store.state.posts[this.id];
}
}
};
Comments
The benefits I see in this is mainly the encapsulation of everything related to the component. It knows how to get its data and it is stand alone, all I need is to pass it an id.
On the other hand, knowing too much is problematic, a lot of things outside of the scope of the component could break it.
Second Approach: the component knows nothing about the store:
In this approach the component would get everything as a property: id, title, text, and state to tell it if it should render inputs or just text fields.
And instead of dispatching actions it would maybe emit events.
Example Code
const IgnorantPost = {
name: 'Post',
props: ['id', 'title', 'text', 'state'],
data() {
return {
post: {
title: '',
text: '',
},
internalState: 'show'
}
},
methods: {
updateClicked() {
this.internalState = 'update';
},
saveClicked() {
this.internalState = 'show';
this.$emit('saving', { id: this.id, ...this.post });
},
},
created() {
this.post.title = this.title;
this.post.text = this.text;
this.internalState = this.state;
}
};
Comments
While this solves the dependencies problem, it just pushes some of the logic to the parent component like handling if states of the Post component.
Also if this parent has many children other than Post, it'd become a very fat logic container.
The End
Note that my component is a lot more complex, any ideas on how to approach this particular problem?
Thanks in advance, and I appreciate you reading so far.
You're pretty much in the area of "primarily opinion-based," but I'll put in my two cents: The store is a global; lean toward having things manipulate it directly, rather than putting layers between it and components.
However, if the components you're using to implement your program have much promise of reusability, you might want to implement them in the usual encapsulated way, pushing the store interaction up.
But mostly I think manipulate the store directly.
Related
Given the code below, my child component alerts trigger before any of the code in the Parent mounted function.
As a result it appears the child has already finished initialization before the data is ready and therefor won't display the data until it is reloaded.
The data itself comes back fine from the API as the raw JSON displays inside the v-card in the layout.
My question is how can I make sure the data requested in the Parent is ready BEFORE the child component loads? Anything I have found focuses on static data passed in using props, but it seems this completely fails when the data must be fetched first.
Inside the mounted() of the Parent I have this code which is retrieves the data.
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray).then(() => {
console.log('DATA ...') // fires after the log in Notes component
this.checkAttendanceForPreviousTwoWeeks().then(()=>{
this.getCurrentParticipants().then((results) => {
this.currentP = results
this.notesArr = this.notes // see getter below
})
The getter that retrieves the data in the parent
get notes() {
const newNotes = eventsModule.getNotes
return newNotes
}
My component in the parent template:
<v-card light elevation="">
{{ notes }} // Raw JSON displays correctly here
// Passing the dynamic data to the component via prop
<Notes v-if="notes.length" :notesArr="notes"/>
</v-card>
The Child component:
...
// Pickingn up prop passed to child
#Prop({ type: Array, required: true })
notesArr!: object[]
constructor()
{
super();
alert(`Notes : ${this.notesArr}`) // nothing here
this.getNotes(this.notesArr)
}
async getNotes(eventNotes){
// THIS ALERT FIRES BEFORE PROMISES IN PARENT ARE COMPLETED
alert(`Notes.getNotes CALL.. ${eventNotes}`) // eventNotes = undefined
this.eventChanges = await eventNotes.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
...
I am relatively new to Vue so forgive me if I am overlooking something basic. I have been trying to fix it for a couple of days now and can't figure it out so any help is much appreciated!
If you are new to Vue, I really recommend reading the entire documentation of it and the tools you are using - vue-class-component (which is Vue plugin adding API for declaring Vue components as classes)
Caveats of Class Component - Always use lifecycle hooks instead of constructor
So instead of using constructor() you should move your code to created() lifecycle hook
This should be enough to fix your code in this case BUT only because the usage of the Notes component is guarded by v-if="notes.length" in the Parent - the component will get created only after notes is not empty array
This is not enough in many cases!
created() lifecycle hook (and data() function/hook) is executed only once for each component. The code inside is one time initialization. So when/if parent component changes the content of notesArr prop (sometimes in the future), the eventChanges will not get updated. Even if you know that parent will never update the prop, note that for performance reasons Vue tend to reuse existing component instances when possible when rendering lists with v-for or switching between components of the same type with v-if/v-else - instead of destroying existing and creating new components, Vue just updates the props. App suddenly looks broken for no reason...
This is a mistake many unexperienced users do. You can find many questions here on SO like "my component is not reactive" or "how to force my component re-render" with many answers suggesting using :key hack or using a watcher ....which sometimes work but is almost always much more complicated then the right solution
Right solution is to write your components (if you can - sometimes it is not possible) as pure components (article is for React but the principles still apply). Very important tool for achieving this in Vue are computed propeties
So instead of introducing eventChanges data property (which might or might not be reactive - this is not clear from your code), you should make it computed property which is using notesArr prop directly:
get eventChanges() {
return this.notesArr.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
Now whenever notesArr prop is changed by the parent, eventChanges is updated and the component will re-render
Notes:
You are overusing async. Your getNotes function does not execute any asynchronous code so just remove it.
also do not mix async and then - it is confusing
Either:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray)
await this.checkAttendanceForPreviousTwoWeeks()
const results = await this.getCurrentParticipants()
this.currentP = results
this.notesArr = this.notes
or:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
Promise.all(promisesArray)
.then(() => this.checkAttendanceForPreviousTwoWeeks())
.then(() => this.getCurrentParticipants())
.then((results) => {
this.currentP = results
this.notesArr = this.notes
})
Great learning resource
I'm new to React. I'm not understanding a part in this code:
var HelloMessage = React.createClass({
render: function() {
return <h2>{this.props.message}</h2>;
}
});
var TextBox = React.createClass({
getInitialState: function() {
return { isEditing: false }
},
update: function() {
// Where is props.update defined? //
this.props.update(this.refs.messageTextBox.value);
this.setState(
{
isEditing: false
});
},
edit: function() {
this.setState({ isEditing: true});
},
In the code I can't find the props declaration for update. But looking through the code we should see "update" as a property of the TextBox component.
I'm not seeing an explicit declaration of this.props.update within any render method.
How/Where is props.update defined?
So inside the HelloReact component render method, a few TextBox components are returned like so:
...
<TextBox label='First Name' update={this.update.bind(null, 'firstName')}>
...
Now here what is happening is that HelloReact is passing a prop named update to this TextBox component. This means that inside the TextBox component I will be able to use this prop with this.props.update. Every prop that is passed down from the parent will populate this.props of the child. In this specific case we are passing down label and update.
Now inside the TextBox component we will be able to access these props intuitively with this.props.label and this.props.update. Inside this component it's define a private method that is called update, this is the code from the snippet you posted with better formatting:
...
update: function() {
this.props.update(this.refs.messageTextBox.value);
this.setState({ isEditing: false });
},
...
So here we are calling this.props.update which is the prop that was passed down from the parent in HelloReact. The reason we are wrapping this call in a private method is because on top of being able to call this.props.update() we also want to do something else, in this case we want to update the state of the TextBox component as well.
I hope this explanation was clear enough. I suggest reading about React from the official docs which are pretty amazing, or watch any of the many tutorials online. These are key concepts of React and you need to understand them properly in order to be able to develop in React.
For this case you might wanna read this, which comes from the offical docs and is about props.
We have a list of lectures and chapters where the user can select and deselect them. The two lists are stored in a redux store.
Now we want to keep a representation of selected lecture slugs and chapter slugs in the hash tag of the url and any changes to the url should change the store too (two-way-syncing).
What would be the best solution using react-router or even react-router-redux?
We couldn't really find some good examples where the react router is only used to maintain the hash tag of an url and also only updates one component.
I think you don’t need to.
(Sorry for a dismissive answer but it’s the best solution in my experience.)
Store is the source of truth for your data. This is fine.
If you use React Router, let it be the source of truth for your URL state.
You don’t have to keep everything in the store.
For example, considering your use case:
Because the url parameters only contain the slugs of the lectures and the chapters which are selected. In the store I have a list of lectures and chapters with a name, slug and a selected Boolean value.
The problem is you’re duplicating the data. The data in the store (chapter.selected) is duplicated in the React Router state. One solution would be syncing them, but this quickly gets complex. Why not just let React Router be the source of truth for selected chapters?
Your store state would then look like (simplified):
{
// Might be paginated, kept inside a "book", etc:
visibleChapterSlugs: ['intro', 'wow', 'ending'],
// A simple ID dictionary:
chaptersBySlug: {
'intro': {
slug: 'intro',
title: 'Introduction'
},
'wow': {
slug: 'wow',
title: 'All the things'
},
'ending': {
slug: 'ending',
title: 'The End!'
}
}
}
That’s it! Don’t store selected there. Instead let React Router handle it. In your route handler, write something like
function ChapterList({ chapters }) {
return (
<div>
{chapters.map(chapter => <Chapter chapter={chapter} key={chapter.slug} />)}
</div>
)
}
const mapStateToProps = (state, ownProps) => {
// Use props injected by React Router:
const selectedSlugs = ownProps.params.selectedSlugs.split(';')
// Use both state and this information to generate final props:
const chapters = state.visibleChapterSlugs.map(slug => {
return Object.assign({
isSelected: selectedSlugs.indexOf(slug) > -1,
}, state.chaptersBySlug[slug])
})
return { chapters }
}
export default connect(mapStateToProps)(ChapterList)
react-router-redux can help you inject the url stuff to store, so every time hash tag changed, store also.
I have UI like this:
First User: <UserSelect value={this.state.user1} />
Second User: <UserSelect value={this.state.user2} />
...
Other User: <UserSelect value={this.state.user3} />
Where UserSelect is component for selecting user with autocomplete. There can be many of them. One type of component can listen singleton Store in Flux. But in my case the change of value in one UserSelect component affects the states of the others.
How should I build my app to solve this problem?
Create one store per component in componentDidMount?
// not flux way
componenDidMount: function() {
this.usersStore = UsersStore.createStore();
this.usersStore.on('change', ...);
}
Or making something like query selectors in Store?
// flux way, but affects all mounted components on page
componenDidMount: function() {
UsersStore.on('change', this.update);
},
update: function() {
this.setState({
items: UserStore.get({ loginLike: this.state.inputValue })
});
},
handleInputChange: function(e) {
this.setState({ inputValue: e.target.value });
loadUsersAction({ loginLike: e.target.value });
}
Or ...?
You can use the second approach and do something like query selectors and be fine. Just don't forget about the correct Flux flow.
If you want to have really great performance and easier development in long-run, use immutable data structures. Then, you will not have to worry about affecting other components (regarding to the performance). Here is a simple introduction article: http://blog.risingstack.com/the-react-js-way-flux-architecture-with-immutable-js/
I have been slowly converting my React+Flux apps to use Immutable.js data structures. I use the original, vanilla FB implementation of Flux.
One problem I have encountered is mixing component state with state received from Flux stores.
I keep all important business-logic state in stores. But my rule has been to keep UI-related state within components. Stores don't need to concern themselves if, for example, a dropdown menu is open or not, right?
The problem comes when an action is taken in a component that changes state in that same component's store. Let's say we have a component with a dropdown menu that is open. An item is selected from that dropdown menu. The action propagates to the ItemStore, the store emits a change, and the component gets new state from the store.
_onChange() {
this.setState(this._getState());
}
_getState() {
if(this.state === undefined) {
return {
data: Immutable.Map({
selectedItem: ItemStore.getSelectedItem(),
items: ItemStore.getItems(),
menuIsOpen: false
})
};
}
return {
data: this.state.data.merge(Immutable.Map({
selectedItem: ItemStore.getSelectedItem(),
items: ItemStore.getItems(),
menuIsOpen: this.state.data.get("menuIsOpen")
}))
};
}
Concurrently, in the component, the click on the dropdown menu item emits an old-fashioned onClick event. I have a _handleClick function which uses setState to close the dropdown menu (local state).
_handleClick(event) {
event.preventDefault();
this.setState({
data: this.state.data.set("menuIsOpen", !this.state.data.get("menuIsOpen"))
});
}
The problem is that _handleClick ends up being called so soon after _getState that it doesn't have an updated copy of this.state.data. So in the component's render method, this.state.data.get("selectedItem") still shows the previously-selected item.
When I do this with POJOs, React's setState seems to batch everything correctly, so it was never an issue. But I don't want to have state that is not part of an Immutable.Map, because I want to take advantage of "pure" rendering. Yet I don't want to introduce UI state into my stores, because I feel like that could get messy real quick.
Is there a way I could fix this? Or is it just a bad practice to merge local Immutable.Map state and Immutable.Map store state within a single component?
RELATED: I am not a fan of the boilerplate if(this.state === undefined) logic to set initial local menuIsOpen state in my _getState method. This may be a sign that I am trying to do something that is not correct.
You can pass a callback to setState to enqueue an atomic update.
_onChange() {
this.setState(state => this._getState(state));
}
_getState(state) {
if(state === undefined) {
return {
data: Immutable.Map({
selectedItem: ItemStore.getSelectedItem(),
items: ItemStore.getItems(),
menuIsOpen: false
})
};
}
return {
data: state.data.merge(Immutable.Map({
selectedItem: ItemStore.getSelectedItem(),
items: ItemStore.getItems(),
menuIsOpen: state.data.get("menuIsOpen")
}))
};
}
About your related point, you might want to take a look at getInitialState.
Why have 2 separate actions occur when you click (fire action to store, close menu)? Instead, you could say, when they click a menu item, we need to close the menu item, and alert the store of the new value.
_handleClick(event) {
event.preventDefault();
this.setState({
data: this.state.data.set("menuIsOpen", false)
}, function() {
alertTheSelectionChange(selectedItem)
});
}