I am very new to State Management. I'm currently building a product list with infinite loading features. These are my codes:
This is my component:
<template>
<div class="">
<section class="space--sm">
<div class="container">
<div class="row">
<div class="col-sm-12">
<div v-for="(data, index) in products" v-bind:key="data.id" class="item col-sm-4 col-xs-12" :id="'data-id-'+data.id" >
<a href="#" v-on:click.prevent="selectProduct(data)" >
<h4>{{data.name}}</h4>
</a>
</div>
<infinite-loading force-use-infinite-wrapper="true" #infinite="infiniteHandler" ref="infiniteLoading">
<span slot="no-results">
No results
</span>
<span slot="no-more">
There are no more results
</span>
</infinite-loading>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
import InfiniteLoading from 'vue-infinite-loading';
import { mapState, mapActions } from 'vuex';
export default {
name: 'List',
computed: mapState({
products: state => state.list.products
}),
methods: {
...mapActions('list', [
'selectProduct'
]),
infiniteHandler($state) {
setTimeout(() => {
this.$store.dispatch('products/fetch')
console.log(this.products.length);
//returns 0 but vue debugger says products state got datas
if (this.products.length) {
$state.loaded();
if(this.products.length < 15){
$state.complete();
}
} else {
$state.complete();
}
}, 1000);
},
},
components: {
InfiniteLoading
}
}
</script>
This is my store:
import axios from 'axios';
// initial state
const state = {
products: [],
selected_product: [],
page: 1
}
// getters
const getters = {}
// mutations
const mutations = {
FETCH(state, products) {
state.products = state.products.concat(products);
},
selectProduct (state, { product }) {
state.selected_product = product;
},
updatePage (state) {
state.page += 1;
},
}
// actions
const actions = {
fetch({ commit }) {
return axios.get('/api/get/products', {
params: {
page: state.page
}
})
.then((response) => {
commit('updatePage')
commit('FETCH', response.data.data)
})
.catch();
},
selectProduct ({ state, commit }, product) {
commit('selectProduct', { product })
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
In the infiniteHandler method, I did this :
this.$store.dispatch('products/fetch')
Afer mutation products state should have loaded data inside (checked using vue debugger) but when I do this after dispatching :-
console.log(this.products.length);
I get 0 which also means no datas present inside the state. Am I doing anything wrong? Also is this the best practice of using Vuex? Thank you
If an asynchronous action returns a promise (as yours does), it is returned by the call to dispatch so your consumer can wait for the action to complete.
For example
async infiniteHandler($state) { // note, this is an "async" function
setTimeout(() => {
await this.$store.dispatch('products/fetch') // note the "await"
console.log(this.products.length);
if (this.products.length) {
$state.loaded();
if(this.products.length < 15){
$state.complete();
}
} else {
$state.complete();
}
}, 1000);
},
One more thing, your catch() at the end of the fetch action won't do you any favours. I'd advise removing it so your consumers can be made aware of any errors. That, or handle the error in your action but still pass the promise rejection on to your component, eg
.catch(res => {
// handle error, maybe with your state
throw res // make sure to keep rejecting the promise
})
and in your component
try {
await this.$store.dispatch('products/fetch')
// etc
} catch (e) {
// do something else
}
Just noticed another thing... you should use the passed in context state in your actions, not some global state object, ie
fetch({ state, commit }) {
this.$store.dispatch('products/fetch') is asynchronous. console.log(this.products.length); is called before action is finished.
You should use watch in this case.
watch: {
products: function (newVal) {
if (newVal.length) {
$state.loaded();
if(newVal.length < 15){
$state.complete();
}
} else {
$state.complete();
}
}
}
async infiniteHandler($state) {
setTimeout(() => {
await this.$store.dispatch('products/fetch')
console.log(this.products.length);
if (this.products.length) {
setTimeout(() => {
$state.loaded();
}, 1000);
if(this.products.length < 15){
$state.complete();
}
} else {
$state.complete();
}
}, 1000);
},
Related
So I've got this page which puts out all of the profiles on my database and what I want to do is when someone clicks on an individual profile they go in to a profile page which displays the rest of the data for the corresponding id they have just clicked on but I don't seem to be able to get it to work.
Below is my js file to get individual profiles.
import { projectFirestore } from "../Firebase/Config";
import { ref } from "vue"
const getPBasic = (id) => {
const PBasic = ref(null)
const error = ref(null)
const load = async () => {
try{
let res = await projectFirestore.collection('Basic').doc(id).get()
PBasic.value = {...res.data(), id: res.id}
console.log(PBasic.value)
}
catch (err){
error.value = err.message
console.log(error.value)
}
}
return { PBasic, error, load}
}
export default getPBasic
And this is what the vue page which I want the data to appear on after they have clicked on a profile from the previous page.
<script>
import getPBasic from "../Composables/getPBasic";
const {PBasic, error, load} = getPBasic(route.params.id);
load();
export default {
name: "Slider",
data() {
return {
images: [
"/src/assets/sample-1.jpg",
"/src/assets/sample-2.jpg",
"/src/assets/sample-3.jpg",
"/src/assets/sample-4.jpg"
],
currentIndex: 0
};
},
methods: {
next: function() {
this.currentIndex += 1;
},
prev: function() {
this.currentIndex -= 1;
}
},
computed: {
currentImg: function() {
return this.images[Math.abs(this.currentIndex) % this.images.length];
}
}
};
</script>
<template>
<div v-if="error">{{ error }}</div>
<div v-if="PBasic" class="PBasic">
<br><br>
<p>{{ PBasic.Name }} </p>
<p>{{ PBaic.Age }} </p>
</div>
<div v-else>
<spinner/>
</div>
Thats what I've got so far I just shortened it for here so it didn't go on and on for too long, if anyone has any ideas I would greatly appreciate it, Thanks.
You can run your getPBasic function in page load to fetch the data you need.
One possible solution is, Run your getPBasic function in a lifecycle hook that fire on page render like mounted() hook or onMounted() in vue3 script setup.
And as we can see your getPBasic function is a synchronous function so use await to get the return value properly.
One possible code might look like this,
<script>
import getPBasic from "../Composables/getPBasic";
const {PBasic, error, load} = getPBasic(route.params.id);
export default {
name: "Slider",
mounted(){
let {PBasic,error,load} = await getPBasic();
this.PBasic = PBasic;
this.error = error;
this.load = load;
},
data() {
return {
PBasic:{},
error: null,
load: false,
images: [
"/src/assets/sample-1.jpg",
"/src/assets/sample-2.jpg",
"/src/assets/sample-3.jpg",
"/src/assets/sample-4.jpg"
],
currentIndex: 0
};
},
methods: {
next: function() {
this.currentIndex += 1;
},
prev: function() {
this.currentIndex -= 1;
}
},
computed: {
currentImg: function() {
return this.images[Math.abs(this.currentIndex) % this.images.length];
}
}
};
</script>
<template>
<div v-if="error">{{ error }}</div>
<div v-if="PBasic" class="PBasic">
<br><br>
<p>{{ PBasic.Name }} </p>
<p>{{ PBaic.Age }} </p>
</div>
<div v-else>
<spinner/>
</div>
I have difficult to use vuex global state combine with re-render child-component in Vue.js.
The global state is mutated but does not re-render its data in v-for loop.
All list of data is rendered, but when the new data changes, component in /blog does not change data in it.
Here is some code:
/store/index.js
export const state = () => ({
allData: [],
})
export const getters = {
getAllData: (state) => state.allData,
}
export const mutations = {
GET_DATAS(state, payload) {
state.allData = payload
},
UPDATE_DATA(state, payload) {
const item = state.allData[payload.index]
Object.assign(item, payload)
},
}
export const actions = {
getDatas({ commit, state }, payload) {
return fetch(`URL_FETCH`)
.then((data) => data.json())
.then((data) => {
commit('GET_DATAS', data)
})
.catch((err) => console.log(err))
},
updateData({ commit, state }, payload) {
commit('UPDATE_DATA', payload)
},
}
in /layouts/default.vue
beforeCreate() {
this.$store.dispatch('getDatas').then(() => {
connectSocket()
})
},
methods: {
connectSocket() {
// connect & received message from socket
// received message from socket
this.$root.$emit('updateData', {
index: 12,
price: 34,
change: 56,
percent: 78,
})
},
},
and in /pages/blog/index.vue
<template>
<div>
<div
v-for="index in getAllData"
:key="index.name"
class="w-100 grid-wrapper"
>
<div>{{ index.price }}</div>
<div>{{ index.change }}</div>
<div>{{ index.percent }}</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data() {
return {}
},
computed: {
...mapGetters(['getAllData']),
},
mounted() {
this.$root.$on('updateData', (item) => {
this.$store.dispatch('updateData', {
index: item.index,
price: item.price,
percent: item.percent,
change: item.change,
})
})
},
}
</script>
Here is a complete example on how to use Vuex and load the data efficiently into a Nuxt app (subjective but using good practices).
/pages/index.vue
<template>
<div>
<main v-if="!$fetchState.pending">
<div v-for="user in allData" :key="user.id" style="padding: 0.5rem 0">
<span>{{ user.email }}</span>
</div>
</main>
<button #click="fakeUpdate">Update the 2nd user</button>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
data() {
return {
mockedData: {
name: 'John Doe',
username: 'jodoe',
email: 'yoloswag#gmail.com',
phone: '1-770-736-8031 x56442',
website: 'hildegard.org',
},
}
},
async fetch() {
await this.setAllData()
},
computed: {
...mapState(['allData']),
},
methods: {
...mapActions(['setAllData', 'updateData']),
fakeUpdate() {
this.updateData({ index: 1, payload: this.mockedData })
},
},
}
</script>
/store/index.js
import Vue from 'vue'
export const state = () => ({
allData: [],
})
export const mutations = {
SET_ALL_DATA(state, payload) {
state.allData = payload
},
UPDATE_SPECIFIC_DATA(state, { index, payload }) {
Vue.set(state.allData, index, payload)
},
}
export const actions = {
async setAllData({ commit }) {
try {
const httpCall = await fetch('https://jsonplaceholder.typicode.com/users')
const response = await httpCall.json()
commit('SET_ALL_DATA', response)
} catch (e) {
console.warn('error >>', e)
}
},
updateData({ commit }, { index, payload }) {
commit('UPDATE_SPECIFIC_DATA', { index, payload })
},
}
I'm trying to fetch some data from an API when some value is updated in a parent component, then use it in a child component. I tried several things but none worked.
Here's a simplified version of my components:
Parent
<template lang="html">
<div id="wrapper">
<h4>My Super Component</h4>
<button v-on:click="setListID">Load another list</button>
<ChildComponent :usernames="usernames"></ChildComponent>
</div>
</template>
<script>
import ChildComponent from "./ChildComponent.vue"
export default {
components: {
ChildComponent
},
data() {
return {
listID: 0,
usernames: undefined,
}
},
watch: {
listID: function(newID) {
this.usernames = getUsernames(newID)
}
},
methods: {
setListID() {
let id = +prompt("Input the list ID");
if (Number.isNaN(id)) {
alert("Please input a valid number");
} else {
this.listID = id;
}
}
},
async mounted() {
this.usernames = await getUsernames(this.listID)
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Simulating an API call
async function getUsernames(listID) {
sleep(200).then(() => {
switch (listID) {
case 0:
return ['Pierre', 'Paul', 'Jaques']
case 1:
return ['Riri', 'Fifi', 'Loulou']
case 2:
return ['Alex', 'Sam', 'Clover']
default:
return []
}
})
}
</script>
Child
<template lang="html">
<p v-for="username in usernames">{{username}}</p>
</template>
<script>
export default {
props: {
usernames: Object
},
}
</script>
The props I get in the child is a Promise, I tried to pass an Array but as the function that fetches the data is async, and I can't await from watch, I'm kinda stuck.
UPDATE:
I think the issue comes from this code:
// Simulating an API call
async function getUsernames(listID) {
await sleep(200).then(() => {
switch (listID) {
case 0:
return ['Pierre', 'Paul', 'Jaques']
case 1:
return ['Riri', 'Fifi', 'Loulou']
case 2:
return ['Alex', 'Sam', 'Clover']
default:
return []
}
})
return 'returned too early'
}
The function always returns 'returned too early'. When I remove this default return, undefined is returned and my child component uses it as the array.
Try like following snippet
Vue.component('Child', {
template: `
<div class="">
<p v-for="username in usernames">{{username}}</p>
</div>
`,
props: {
usernames: Array
},
})
new Vue({
el: '#demo',
data() {
return {
listID: 0,
usernames: undefined,
}
},
watch: {
listID: async function(newID) {
this.usernames = await this.getUsernames(newID)
}
},
methods: {
setListID() {
let id = +prompt("Input the list ID");
if (Number.isNaN(id)) {
alert("Please input a valid number");
} else {
this.listID = Number(id);
}
},
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
getUsernames(listID) {
return this.sleep(200).then(() => {
switch (listID) {
case 0:
return ['Pierre', 'Paul', 'Jaques']
case 1:
return ['Riri', 'Fifi', 'Loulou']
case 2:
return ['Alex', 'Sam', 'Clover']
default:
return []
}
})
}
},
async mounted() {
this.usernames = await this.getUsernames(this.listID)
}
})
Vue.config.productionTip = false
Vue.config.devtools = false
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<div id="wrapper">
<h4>My Super Component</h4>
<button v-on:click="setListID">Load another list</button>
<Child :usernames="usernames"></ChildComponent>
</div>
</div>
Seems like an issue with props you sending array but in the child component you expecting object.
try below code and see
<template lang="HTML">
<p v-for="(username, index) in usernames" :key="index">{{username}}</p>
</template>
<script>
export default {
props: {
type: Object, // or Aarray
default: () => {
return {} // [] if the type is array
}
},
}
</script>
I'm building a simple note-taking app and I'm trying to add new note at the end of the list of notes, and then see the added note immediately. Unfortunately I'm only able to do it by refreshing the page. Is there an easier way?
I know that changing state would usually help, but I have two separate components and I don't know how to connect them in any way.
So in the NewNoteForm component I have this submit action:
doSubmit = async () => {
await saveNote(this.state.data);
};
And then in the main component I simply pass the NewNoteForm component.
Here's the whole NewNoteForm component:
import React from "react";
import Joi from "joi-browser";
import Form from "./common/form";
import { getNote, saveNote } from "../services/noteService";
import { getFolders } from "../services/folderService";
class NewNoteForm extends Form {
//extends Form to get validation and handling
state = {
data: {
title: "default title",
content: "jasjdhajhdjshdjahjahdjh",
folderId: "5d6131ad65ee332060bfd9ea"
},
folders: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string().label("Title"),
content: Joi.string()
.required()
.label("Note"),
folderId: Joi.string()
.required()
.label("Folder")
};
async populateFolders() {
const { data: folders } = await getFolders();
this.setState({ folders });
}
async populateNote() {
try {
const noteId = this.props.match.params.id;
if (noteId === "new") return;
const { data: note } = await getNote(noteId);
this.setState({ data: this.mapToViewModel(note) });
} catch (ex) {
if (ex.response && ex.response.status === 404)
this.props.history.replace("/not-found");
}
}
async componentDidMount() {
await this.populateFolders();
await this.populateNote();
}
mapToViewModel(note) {
return {
_id: note._id,
title: note.title,
content: note.content,
folderId: note.folder._id
};
}
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}
doSubmit = async () => {
await saveNote(this.state.data);
};
render() {
return (
<div>
<h1>Add new note</h1>
<form onSubmit={this.handleSubmit}>
{this.renderSelect("folderId", "Folder", this.state.folders)}
{this.renderInput("title", "Title")}
{this.renderInput("content", "Content")}
{this.renderButton("Add")}
</form>
</div>
);
}
}
export default NewNoteForm;
And here's the whole main component:
import React, { Component } from "react";
import { getNotes, deleteNote } from "../services/noteService";
import ListGroup from "./common/listGroup";
import { getFolders } from "../services/folderService";
import { toast } from "react-toastify";
import SingleNote from "./singleNote";
import NewNoteForm from "./newNoteForm";
class Notes extends Component {
state = {
notes: [], //I initialize them here so they are not undefined while componentDidMount is rendering them, otherwise I'd get a runtime error
folders: [],
selectedFolder: null
};
async componentDidMount() {
const { data } = await getFolders();
const folders = [{ _id: "", name: "All notes" }, ...data];
const { data: notes } = await getNotes();
this.setState({ notes, folders });
}
handleDelete = async note => {
const originalNotes = this.state.notes;
const notes = originalNotes.filter(n => n._id !== note._id);
this.setState({ notes });
try {
await deleteNote(note._id);
} catch (ex) {
if (ex.response && ex.response.status === 404)
toast.error("This note has already been deleted.");
this.setState({ notes: originalNotes });
}
};
handleFolderSelect = folder => {
this.setState({ selectedFolder: folder }); //here I say that this is a selected folder
};
render() {
const { selectedFolder, notes } = this.state;
const filteredNotes =
selectedFolder && selectedFolder._id //if the selected folder is truthy I get all the notes with this folder id, otherwise I get all the notes
? notes.filter(n => n.folder._id === selectedFolder._id)
: notes;
return (
<div className="row m-0">
<div className="col-3">
<ListGroup
items={this.state.folders}
selectedItem={this.state.selectedFolder} //here I say that this is a selected folder
onItemSelect={this.handleFolderSelect}
/>
</div>
<div className="col">
<SingleNote
filteredNotes={filteredNotes}
onDelete={this.handleDelete}
/>
<NewNoteForm />
</div>
</div>
);
}
}
export default Notes;
How can I connect these two components so that the data shows smoothly after submitting?
You can use a callback-like pattern to communicate between a child component and its parent (which is the 3rd strategy in #FrankerZ's link)
src: https://medium.com/#thejasonfile/callback-functions-in-react-e822ebede766)
Essentially you pass in a function into the child component (in the main/parent component = "Notes": <NewNoteForm onNewNoteCreated={this.onNewNoteCreated} />
where onNewNoteCreated can accept something like the new note (raw data or the response from the service) as a parameter and saves it into the parent's local state which is in turn consumed by any interested child components, i.e. ListGroup).
Sample onNewNoteCreated implementation:
onNewNoteCreated = (newNote) => {
this.setState({
notes: [...this.state.notes, newNote],
});
}
Sample use in NewNoteForm component:
doSubmit/handleSubmit = async (event) => {
event.preventDefault();
event.stopPropagation();
const newNote = await saveNote(this.state.data);
this.props.onNewNoteCreated(newNote);
}
You probably want to stop the refresh of the page on form submit with event.preventDefault() and event.stopPropagation() inside your submit handler (What's the difference between event.stopPropagation and event.preventDefault?).
Hi guys I'm having trouble about my little project, so basically I just need to show to the user the sign up modal if they are not signed in when they click any item on my page. The problem is after they signed in the sign up modal still shows when they click something which should not be since the already signed in.
Here are my codes
SignupModal.vue
axios.get('/user/sign-up', {
params:{
name: this.name,
email: this.email,
password: this.password,
phone: this.phone,
age: this.age,
city: this.city,
}
})
.then( (response) => {
if( response.status == 200 ) {
$('#modals-signup').modal('hide')
this.$store.commit( 'UPDATE_USER_LOGIN_STATE' )
}
});
HomePage.vue
import { mapState } from 'vuex'
export default {
data: function() {
return {
loginState: this.$store.state.isLoggedIn
}
},
computed: {
...mapState([
'isLoggedIn'
]),
},
watch: {
isLoggedIn( newVal, oldVal ) {
this.loginState = newVal
console.log('HOME PAGE WATCH ==================' + newVal + ' ===== ' +this.loginState)
if ( this.loginState ) {
console.log('true since its logged in')
return
}
else {
console.log('false since its logged out')
this.$store.dispatch('preventClickingItems')
}
}
}
}
Store.js
const state = {
isLoggedIn: false,
}
const getters = {
isLoggedIn: (state) => state.isLoggedIn
}
const actions = {
preventClickingItems: ( {commit} ) => {
$('section').on('click', 'div.container *', (e) => {
$('#modals-signup').modal('show')
e.preventDefault()
e.stopPropagation()
return false
})
}
}
const mutations = {
UPDATE_USER_LOGIN_STATE( state ) {
state.isLoggedIn = !state.isLoggedIn
console.log('===========', state.isLoggedIn)
}
}
export default new Vuex.Store({
state,
getters,
actions,
mutations,
plugins: [vuexCookie.plugin]
})
I was able to update the state after user is signed in and able to execute the if block only from the watch, but I dont understand why it still triggers the modal since it only runs the if block.
I have solved this by adding if else statement inside the click event to check the isLoggedIn state value
const actions = {
preventClickingItems: ( {commit, state} ) => {
$('section').on('click', 'div.container *', (e) => {
if ( state.isLoggedIn ) {
return
}
else {
$('#modals-signup').modal('show')
e.preventDefault()
e.stopPropagation()
return false
}
})
}
}