Search code examples
javascriptfirebasevue.jsvuex

Vuex load existing form data from state to edit


I followed through this tutorial to try and learn Vue, I've finished and it works, but I'm trying to make a change that I'm struggling with.

https://savvyapps.com/blog/definitive-guide-building-web-app-vuejs-firebase

So there's a "settings" page which has the user profile (they can edit their name etc). When that "settings" / "profile" page loads I want the form to load their existing data so they can just modify it and press save.

It currently loads as a placeholder with :placeholder="userProfile.name" - I want it to just fill the form with the actual value instead of having it as a placeholder.

It feels like it should be ridiculously simple to do this but can't get it working elegantly.

Settings.vue

<template>
  <section id="settings">
    <div class="col1">
      <h3>Settings</h3>
      <p>Update your profile</p>

      <transition name="fade">
        <p v-if="showSuccess" class="success">profile updated</p>
      </transition>

      <form @submit.prevent>
        <label for="name">Name</label>
        <input v-model.trim="name" type="text" id="name" />

        <label for="title">Job Title</label>
        <input v-model.trim="title" type="text" id="title" />

        <button @click="updateProfile()" class="button">Update Profile</button>
      </form>
    </div>
  </section>
</template>

<script>
import { mapState } from "vuex";

export default {
  data() {
    return {
      name: "",
      title: "",
      showSuccess: false,
    };
  },
  computed: {
    ...mapState(["userProfile"]),
  },
  methods: {
    updateProfile() {
      this.$store.dispatch("updateProfile", {
        name: this.name !== "" ? this.name : this.userProfile.name,
        title: this.title !== "" ? this.title : this.userProfile.title,
      });

      this.name = "";
      this.title = "";

      this.showSuccess = true;

      setTimeout(() => {
        this.showSuccess = false;
      }, 2000);
    },
  },
};
</script>

<style lang="scss" scoped>
</style>

I tried changing the data section to this, which works when I leave the page and go back to it, but if I refresh the page (F5) the fields are blank until I leave the page and come back again.

data() {
    return {
      name: this.$store.state.userProfile.name,
      title: this.$store.state.userProfile.title,
      showSuccess: false,
    };
  },

And here's my store if you need to see that:

store/index.js

import Vue from "vue";
import Vuex from "vuex";
import * as fb from "../firebase";
import router from "../router/index";

Vue.use(Vuex);

// realtime firebase connection
fb.postsCollection.orderBy("createdOn", "desc").onSnapshot((snapshot) => {
  let postsArray = [];

  snapshot.forEach((doc) => {
    let post = doc.data();
    post.id = doc.id;

    postsArray.push(post);
  });

  store.commit("setPosts", postsArray);
});

const store = new Vuex.Store({
  state: {
    userProfile: {},
    posts: [],
  },
  mutations: {
    setUserProfile(state, val) {
      state.userProfile = val;
    },
    setPosts(state, val) {
      state.posts = val;
    },
  },
  actions: {
    async signup({ dispatch }, form) {
      // sign user up
      const { user } = await fb.auth.createUserWithEmailAndPassword(
        form.email,
        form.password
      );

      // create user profile object in userCollections
      await fb.usersCollection.doc(user.uid).set({
        name: form.name,
        title: form.title,
      });

      // fetch user profile and set in state
      dispatch("fetchUserProfile", user);
    },
    async login({ dispatch }, form) {
      // sign user in
      const { user } = await fb.auth.signInWithEmailAndPassword(
        form.email,
        form.password
      );

      // fetch user profile and set in state
      dispatch("fetchUserProfile", user);
    },
    async logout({ commit }) {
      await fb.auth.signOut();

      // clear userProfile and redirect to /login
      commit("setUserProfile", {});
      router.push("/login");
    },
    async fetchUserProfile({ commit }, user) {
      // fetch user profile
      const userProfile = await fb.usersCollection.doc(user.uid).get();

      // set user profile in state
      commit("setUserProfile", userProfile.data());

      // change route to dashboard
      if (router.currentRoute.path === "/login") {
        router.push("/");
      }
    },
    async createPost({ state }, post) {
      await fb.postsCollection.add({
        createdOn: new Date(),
        content: post.content,
        userId: fb.auth.currentUser.uid,
        userName: state.userProfile.name,
        comments: 0,
        likes: 0,
      });
    },
    async likePost(context, { id, likesCount }) {
      const userId = fb.auth.currentUser.uid;
      const docId = `${userId}_${id}`;

      // check if user has liked post
      const doc = await fb.likesCollection.doc(docId).get();
      if (doc.exists) {
        return;
      }

      // create post
      await fb.likesCollection.doc(docId).set({
        postId: id,
        userId: userId,
      });

      // update post likes count
      fb.postsCollection.doc(id).update({
        likes: likesCount + 1,
      });
    },
    async updateProfile({ dispatch }, user) {
      const userId = fb.auth.currentUser.uid;
      // update user object
      /*const userRef = */await fb.usersCollection.doc(userId).update({
        name: user.name,
        title: user.title,
      });

      dispatch("fetchUserProfile", { uid: userId });

      // update all posts by user
      const postDocs = await fb.postsCollection
        .where("userId", "==", userId)
        .get();
      postDocs.forEach((doc) => {
        fb.postsCollection.doc(doc.id).update({
          userName: user.name,
        });
      });

      // update all comments by user
      const commentDocs = await fb.commentsCollection
        .where("userId", "==", userId)
        .get();
      commentDocs.forEach((doc) => {
        fb.commentsCollection.doc(doc.id).update({
          userName: user.name,
        });
      });
    },
  },
  modules: {},
});

export default store;

EDIT

I should have mentioned that this data is being loaded into the state from a Firebase Firestore. It looks like it's just a timing thing, the data isn't quite loaded by the time it sets the data() on the component - I added some console logs.

Fetching user profile.. Settings.vue?e12e:29
Setting Data... index.js?4360:75
Performing setUserProfile commit.. index.js?4360:29
Setting user profile in state, last step..

Again just don't know enough about Vue yet to know how to best change that order..


Solution

  • v-model gets and sets the value of whatever you pass to it. Since you want to edit a state property, as soon as you modify the <input>'s value it will try to change (a.k.a. mutate) the value of the state property. And that would break the immutability principle [1].
    The solution here is to pass a computed property to v-model which uses a getter and a setter, where you tell the component where to get the value from and how to update it.

    By default computed properties are a shorthand and only contain the getter. Basically,

    computed: {
      name() {
        return this.$store.state.userProfile.name
      }
    }
    

    ...can be written as:

    computed: {
      name: {
        get() {
          return this.$store.state.userProfile.name
        }
      }
    }
    

    And what you need is to add a setter which commits the appropriate mutation so the state is updated:

    computed: {
      ...mapState(["userProfile"]),
      name: {
        get() {
          return this.userProfile.name
        },
        set(val) {
          this.$store.commit('setUserProfile', {
            ...this.userProfile,
            name: val
          });
        }
      },
      title: {
        get() {
          return this.userProfile.title
        },
        set(val) {
          this.$store.commit('setUserProfile', {
            ...this.userProfile,
            title: val
          });
        }
      }
    }
    

    Computed setters are documented here.


    [1] - the reason why you're using Vuex is because you don't want to allow any component to directly modify your data. Instead, you want them to commit mutations to the state so that every component using that data gets notified of the change. If you allowed v-model to change your data directly, you'd be breaking the immutability principle, so your state would cease to be the only source of truth.