Categories
Mobx

State Management with MobX 6 — Do Things When an Observable Meets a Given Condition

MobX is a simple state management solution for JavaScript apps.

In this article, we’ll look at how to use MobX 6 to add state management into our JavaScript apps.

Do Things When an Observable Meets a Given Condition

We can use the when function provided by MobX 6 to do something when the store’s state is in a given condition.

For instance, we can write:

import { makeObservable, observable, computed, action, when } from "mobx";

class Count {
  count = 0;
  get doubleCount() {
    return this.count * 2;
  }
  constructor(count) {
    makeObservable(this, {
      count: observable,
      doubleCount: computed,
      increment: action
    });
    this.count = count;
  }

  increment() {
    this.count++;
  }
}

const store = new Count(1);

when(
  () => store.count === 3,
  () => console.log(`It's 3`)
);

store.increment();
store.increment();

We create a store with the count observable, doubleCount computed state, and the increment action.

Then we instantiate it and assign the returned object to the store .

Next, we call when with a function that returns the condition that we want to look for in the store.

The 2nd callback runs when the condition we return in the function we passed in as the first argument is met.

Since we called increment twice and the store’s count state is initialized to 1, we should see the console log run.

If we don’t pass in a 2nd argument into when , it returns a promise.

So we can use it to wait for a given condition and then run code after that.

For instance, we can write:

import { makeObservable, observable, computed, action, when } from "mobx";

class Count {
  count = 0;
  get doubleCount() {
    return this.count * 2;
  }
  constructor(count) {
    makeObservable(this, {
      count: observable,
      doubleCount: computed,
      increment: action
    });
    this.count = count;
  }

  increment() {
    this.count++;
  }
}

const store = new Count(1);

const when3 = async () => {
  await when(() => store.count === 3);
  console.log(`It's 3`);
};

when3();
store.increment();
store.increment();

We have the same store as in the previous example.

But below that, we have the when3 function that calls when with the same function as before as the first argument.

The 2nd function is omitted.

Then we call the console.log after that.

Then we run the when3 function.

Next, we call increment twice as before.

So we should see the console log run after count reaches 3 as we specified in the when3 function.

Conclusion

We can use the when function provided by MobX 6 to do something after a store’s state meets the condition we’re looking for.

Categories
Mobx

State Management with MobX 6 — Disposing Autorun and Watching Observables

MobX is a simple state management solution for JavaScript apps.

In this article, we’ll look at how to use MobX 6 to add state management into our JavaScript apps.

Disposing Autorun

When we don’t need to watch a store for changes anymore, we should dispose of any watchers we created.

To do this, we can call the function returned by autorun , which returns a function that lets us remove it from memory when we’re done using it.

For instance, we can write:

import { makeObservable, observable, computed, action, autorun } from "mobx";

class Count {
  count = 0;
  get doubleCount() {
    return this.count * 2;
  }
  constructor(count) {
    makeObservable(this, {
      count: observable,
      doubleCount: computed,
      increment: action
    });
    this.count = count;
  }

  increment() {
    this.count++;
  }
}

const store = new Count(1);

const dispose = autorun(() => {
  console.log(store.count);
});

store.increment();
dispose();

We create a store with the count observable, doubleCount computed state, and the increment action.

Then we instantiate it and assign the returned object to the store .

Next, we call autorun to watch the values we want.

We assign the returned function to the dispose variable.

Then we call the store.increment action to increment the count state value which will trigger the autorun callback to run.

Then we call dispose to remove the watcher created by autorun .

Watching Observables

In addition to using the autorun function to watch MobX store states, we can use the reaction function.

It takes a function that returns the value we want to watch as the first argument.

The first argument is another function that has the current and previous value being observed as the parameters.

And then we can do something with them in the callback.

For instance, we can write:

import {
  makeObservable,
  observable,
  computed,
  action,
  autorun,
  reaction
} from "mobx";

class Count {
  count = 0;
  get doubleCount() {
    return this.count * 2;
  }
  constructor(count) {
    makeObservable(this, {
      count: observable,
      doubleCount: computed,
      increment: action
    });
    this.count = count;
  }

  increment() {
    this.count++;
  }
}

const store = new Count(1);

reaction(
  () => store.count,
  (currCount, prevCount) => {
    console.log(currCount, prevCount);
  }
);

store.increment();

We have the same store as before.

But we call the reaction function with the 2 callbacks.

The 1srt callback returns the value of the store.count state.

The 2nd callback has the current and previous values of store.count respectively.

The 2nd callback will run when we call store.increment .

This lets us have more fine-grained control of what we want to watch in the store.

Conclusion

We can watch values with more fine-grained control with the reaction function provided by MobX.

Also, we should dispose of watchers created by the autorun function when we no longer need them.

Categories
Mobx

State Management with MobX 6 — Creating Observable States

MobX is a simple state management solution for JavaScript apps.

In this article, we’ll look at how to use MobX 6 to add state management into our JavaScript apps.

Creating Observable State

We can create one or more observable states by creating a store.

To do this, we write:

import { makeObservable, observable, computed, action, autorun } from "mobx";

class Count {
  count = 0;
  get doubleCount() {
    return this.count * 2;
  }
  constructor(count) {
    makeObservable(this, {
      count: observable,
      doubleCount: computed,
      increment: action
    });
    this.count = count;
  }

  increment() {
    this.count++;
  }
}

const store = new Count(1);

autorun(() => {
  console.log(store.count);
});

store.increment();

We have the Count class that has the count and doubleCount states.

count is an observable state that we can manipulate.

We add this by setting the count property to observable in the makeObservable call.

The doubleCount getter is used to create a computed state, which is a state derived from other observable states.

The increment method is an action as indicated by setting increment to action .

We manipulate observable state values in actions.

In the constructor, we set this.count to count to initialize it.

Next, we create the Count instance, which is the store.

Then we call autorun with a callback to get the store.count value.

The callback will run whenever a state updates.

So when store.increment is called, the callback will run to log the latest value of store.count .

This also works with computed states, so we can write:

import { makeObservable, observable, computed, action, autorun } from "mobx";

class Count {
  count = 0;
  get doubleCount() {
    return this.count * 2;
  }
  constructor(count) {
    makeObservable(this, {
      count: observable,
      doubleCount: computed,
      increment: action
    });
    this.count = count;
  }

  increment() {
    this.count++;
  }
}

const store = new Count(1);

autorun(() => {
  console.log(store.count);
  console.log(store.doubleCount);
});

store.increment();

to watch the value of both states in the autorun callback.

Conclusion

We can create a data store for any client-side JavaScript app with MobX.

Categories
Mobx

Getting Started with React State Management with MobX 6

MobX is a simple state management solution for React apps.

In this article, we’ll look at how to use MobX 6 to add state management into our React apps.

Installation

To get started, we can install the MobX package by running:

yarn add mobx

with Yarn or:

npm install --save mobx

with NPM.

To use it with React, we also need the mobx-react-lite package.

To install it, we run:

npm i mobx-react-lite

We can add a script tag to load the MobX library from CDN.

The script’s URL is:

https://cdnjs.com/libraries/mobx

or:

https://unpkg.com/mobx/dist/mobx.umd.production.min.js

Simple Usage

After we installed the packages, we can use them to store global states of our React app.

For instance, we can write:

import React from "react";
import { makeObservable, observable, computed, action } from "mobx";
import { observer } from "mobx-react-lite";

class Count {
  count = 0;
  get doubleCount() {
    return this.count * 2;
  }
  constructor(count) {
    makeObservable(this, {
      count: observable,
      doubleCount: computed,
      increment: action
    });
    this.count = count;
  }

increment() {
    this.count++;
  }
}

const store = new Count(1);

const Counter = observer(({ store: { count } }) => {
  return <div>{count}</div>;
});

const DoubleCounter = observer(({ store: { doubleCount } }) => (
  <div>{doubleCount}</div>
));

const Incrementer = observer(({ store }) => {
  return (
    <div>
      <button onClick={() => store.increment()}>increment</button>
    </div>
  );
});

export default function App() {
  return (
    <div>
      <Incrementer store={store} />
      <Counter store={store} />
      <DoubleCounter store={store} />
    </div>
  );
}

to create the Count store and use it in our React app.

We have the count state in the Count class.

doubleCount is a state that’s computed from the value of the count state.

In the constructor, we have makeObservable function to make the count and doubleCount properties reactive states.

observable makes it an observable state.

And computed makes it a computed state, which means it’s a reactive state derived from one or more observable states.

Then we initialize the this.count class property to the value of the count parameter.

Next, we create the increment method to increment the count value.

Then we create a store from the Count class and initialize the count state to 1.

Next, we create the Counter component which gets the store.count value and render it.

We also have the DoubleCounter component to render the store.doubleCount value.

Next, we have the Incrementer component that gets the store from the prop and call its increment to increment the count value when we click the button.

We wrap all 3 components with the MobX observer function to make all 3 components watch the store state values.

Finally, in App , we add all 3 components and pass in the store so that when we click increment, we see both count and doubleCount update.

Conclusion

We can add state management to our React app with MobX easily.

Categories
Vue

How to Add Native Notifications to Your Vue.js App

With the HTML5 Notification API, browsers can display native popup notifications to users. With notifications, you can display text and icons, and also play sound with them. The full list of options are located at https://developer.mozilla.org/en-US/docs/Web/API/notification. Users have to grant permission to display notifications when they visit a web app to see browser notifications.

Developers have done the hard work for us if we use React because a React component is created to display browser notifications. The Vue-Native-Notification package, located at https://www.npmjs.com/package/vue-native-notification can let us display popups and handle the events that are associated with display the notifications like when use clicks on the notification or handle cases when permissions or granted or denied for display notifications.

In this article, we will build a password manager that lets you enter, edit and delete password to the websites and show notifications whenever these actions are taken. We will use Vue.js to build the app.

To start we create the project by running npx @vue/cli create password-manager . In the wizard, choose ‘Manually select features’ and choose to include Babel, Vue Router, and Vuex in our app.

Next we install some libraries we need. We need Axios for making HTTP requests, Bootstrap Vue for styling, V-Clipboard for the copy to clipboard functionality, Vue-Native-Notification for showing native browser notifications and Vee-Validate for form validation. We install them by running:

npm i axios bootstrap-vue v-clipboard vee-validate `vue-native-notification`

After we install the libraries, we can start building the app. First in the components folder, create a file called PasswordForm.vue for our password form. Then in there, we add:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group label="Name">
        <ValidationProvider name="name" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.name"
            required
            placeholder="Name"
            name="name"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Name is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-form-group label="URL">
        <ValidationProvider name="url" rules="required|url" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.url"
            required
            placeholder="URL"
            name="url"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-form-group label="Username">
        <ValidationProvider name="username" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.username"
            required
            placeholder="Username"
            name="username"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Username is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-form-group label="Password">
        <ValidationProvider name="password" rules="required" v-slot="{ errors }">
          <b-form-input
            type="password"
            :state="errors.length == 0"
            v-model="form.password"
            required
            placeholder="Password"
            name="password"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Password is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-button type="submit" variant="primary" style="margin-right: 10px">Submit</b-button>
      <b-button type="reset" variant="danger" @click="cancel()">Cancel</b-button>
    </b-form>
  </ValidationObserver>
</template>

<script>
import { requestsMixin } from "@/mixins/requestsMixin";

export default {
  name: "PasswordForm",
  mixins: [requestsMixin],
  props: {
    edit: Boolean,
    password: Object
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }

      if (this.edit) {
        await this.editPassword(this.form);
        this.$notification.show(
          "Password edited",
          {
            body: "Password edited"
          },
          {}
        );
      } else {
        await this.addPassword(this.form);
        this.$notification.show(
          "Password added",
          {
            body: "Password added"
          },
          {}
        );
      }
      const response = await this.getPasswords();
      this.$store.commit("setPasswords", response.data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    }
  },
  data() {
    return {
      form: {}
    };
  },
  watch: {
    password: {
      handler(p) {
        this.form = JSON.parse(JSON.stringify(p || {}));
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

We have the password form in this component. The form includes name, URL, username, and password fields. All of them are required. We use Vee-Validate to validate the form fields. The ValidationObserver component is for validating the whole form, while the ValidationProvider component is for validating the form fields that it wraps around. The validation rule is specified by the rule prop of each field. We have a special url rule for the URL field. We show the validation error messages when the errors object from the scope slot has a non-zero length. The state prop is for setting the validation state which shows the green when errors has length 0 and red otherwise. The error messages are shown in the b-form-invalid-feedback component.

When the user clicks to Save button, onSubmit function is called. We get the validation state of the form by using this.$refs.observer.validate(); . The ref refers to the ref of the ValidationObserver . If it resolves to true , then we call addPassword or editPassword to save the entry depending on the edit prop. Then we get the passwords by calling getPasswords and then put it in our Vuex store by dispatching the setPasswords mutation. Then we emit the saved event to close the modal in the home page. The notifications are shown by calling this.$notification.show , provided by Vue-Native-Notification. The first argument is the notification title, the second contains the body, and the third argument are optional event handlers that you can add if needed. The full list of event handlers are at https://www.npmjs.com/package/vue-native-notification.

We have a watch block mainly used when an existing entry is being edited, we get the password prop and set it to this.form by making a copy of the prop so that we only update the form object and nothing when data is binding.

Next we create a mixins folder and add requestsMixin.js inside it. In the file, add:

const APIURL = "[http://localhost:3000](http://localhost:3000)";
const axios = require("axios");

export const requestsMixin = {
  methods: {
    getPasswords() {
      return axios.get(`${APIURL}/passwords`);
    },

    addPassword(data) {
      return axios.post(`${APIURL}/passwords`, data);
    },

    editPassword(data) {
      return axios.put(`${APIURL}/passwords/${data.id}`, data);
    },

    deletePassword(id) {
      return axios.delete(`${APIURL}/passwords/${id}`);
    }
  }
};

This contains the code to make the HTTP requests in the back end. We include this mixin in our components so that we can make requests to back end from them.

Next in Home.vue , we replace the existing code with:

<template>
  <div class="page">
    <h1 class="text-center">Password Manager</h1>
    <b-button-toolbar>
      <b-button @click="openAddModal()">Add Password</b-button>
    </b-button-toolbar>
    <br />
    <b-table-simple responsive>
      <b-thead>
        <b-tr>
          <b-th>Name</b-th>
          <b-th>URL</b-th>
          <b-th>Username</b-th>
          <b-th>Password</b-th>
          <b-th></b-th>
          <b-th></b-th>
          <b-th></b-th>
          <b-th></b-th>
        </b-tr>
      </b-thead>
      <b-tbody>
        <b-tr v-for="p in passwords" :key="p.id">
          <b-td>{{p.name}}</b-td>
          <b-td>{{p.url}}</b-td>
          <b-td>{{p.username}}</b-td>
          <b-td>******</b-td>
          <b-td>
            <b-button
              v-clipboard="() => p.username"
              @click="notify('Username copied', 'Username copied')"
            >Copy Username</b-button>
          </b-td>
          <b-td>
            <b-button
              v-clipboard="() => p.password"
              @click="notify('Password copied', 'Password copied')"
            >Copy Password</b-button>
          </b-td>
          <b-td>
            <b-button @click="openEditModal(p)">Edit</b-button>
          </b-td>
          <b-td>
            <b-button @click="deleteOnePassword(p.id)">Delete</b-button>
          </b-td>
        </b-tr>
      </b-tbody>
    </b-table-simple>

    <b-modal id="add-modal" title="Add Password" hide-footer>
      <PasswordForm @saved="closeModal()" @cancelled="closeModal()" :edit="false"></PasswordForm>
    </b-modal>

    <b-modal id="edit-modal" title="Edit Password" hide-footer>
      <PasswordForm
        @saved="closeModal()"
        @cancelled="closeModal()"
        :edit="true"
        :password="selectedPassword"
      ></PasswordForm>
    </b-modal>
  </div>
</template>

<script>
import { requestsMixin } from "@/mixins/requestsMixin";
import PasswordForm from "@/components/PasswordForm";

export default {
  name: "home",
  components: {
    PasswordForm
  },
  mixins: [requestsMixin],
  computed: {
    passwords() {
      return this.$store.state.passwords;
    }
  },
  beforeMount() {
    this.getAllPasswords();
  },
  data() {
    return {
      selectedPassword: {}
    };
  },
  methods: {
    notify(title, body) {
      this.$notification.show(
        title,
        {
          body
        },
        {}
      );
    },
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openEditModal(password) {
      this.$bvModal.show("edit-modal");
      this.selectedPassword = password;
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedPassword = {};
    },
    async deleteOnePassword(id) {
      await this.deletePassword(id);
      this.$notification.show(
        "Password deleted",
        {
          body: "Password deleted"
        },
        {}
      );
      this.getAllPasswords();
    },
    async getAllPasswords() {
      const response = await this.getPasswords();
      this.$store.commit("setPasswords", response.data);
    }
  }
};
</script>

In this file, we have a table to display a list of password entries and let users open and close the add and edit modals. We have buttons in each row to copy the username and passwords, and also to let users edit or delete each entry.

In the scripts section, we have the beforeMount hook to get all the password entries during page load with the getPasswords function we wrote in our mixin. When the Edit button is clicked, the selectedPassword variable is set, and we pass it to the PasswordForm for editing.

To delete a password, we call deletePassword in our mixin to make the request to the back end.

The copy to clipboard functionality is added here. For the copy username and password buttons, we use the v-clipboard directive to let us copy the username and password respectively to the clipboard when the button is clicked.

We have notifications for deleting an entry here. They are called the same as in PasswordForm . Also, we added a notify function so that we can show notifications when username or password are copied by click the respective buttons.

Next in App.vue , we replace the existing code with:

<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">Password Manager</b-navbar-brand>

      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>

      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path  == '/'">Home</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template>

<script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>

<style lang="scss">
.page {
  padding: 20px;
}

button {
  margin-right: 10px;
}
</style>

to add a Bootstrap navigation bar to the top of our pages, and a router-view to display the routes we define.

Next in main.js , replace the code with:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import Clipboard from "v-clipboard";
import { required } from "vee-validate/dist/rules";
import VueNativeNotification from "vue-native-notification";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";

extend("required", required);
extend("url", {
  validate: value => {
    return /^(http://www.|https://www.|http://|https://)?[a-z0-9]+([-.]{1}[a-z0-9]+)*.[a-z]{2,5}(:[0-9]{1,5})?(/.*)?$/.test(
      value
    );
  },
  message: "URL is invalid."
});
Vue.use(BootstrapVue);
Vue.use(Clipboard);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(VueNativeNotification, {
  requestOnNotify: true
});
Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

so that we add the libraries we installed to our app so we can use it in our components. We call extend from Vee-Validate to add the form validation rules that we want to use. Also, we add the V-Clipboard library here so we can use it in our home page.

We include the Vue-Native-Notification library here by adding:

Vue.use(VueNativeNotification, {
  requestOnNotify: true
});

The requestOnNotify settings is for showing the permission prompt when the first notification is made if set to true .

In router.js , we replace the existing code with:

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    }
  ]
})

to only include our home page.

Then in store.js , we replace the existing code with:

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    passwords: []
  },
  mutations: {
    setPasswords(state, payload) {
      state.passwords = payload;
    }
  },
  actions: {}
});

to add our passwords state to the store so we can observer it in the computed block of PasswordForm and HomePage components. We have the setPasswords function to update the passwords state and we use it in the components by call this.$store.commit(“setPasswords”, response.data); like we did in PasswordForm . Also, we imported the Bootstrap CSS in this file to get the styles.

Finally, in index.html , replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title>Password Manager</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-clipboard-tutorial-app doesn't work properly
        without JavaScript enabled. Please enable it to continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

to change the title.

After all the hard work, we can start our app by running npm start .

To start the back end, we first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{
  "passwords": [
  ]
}

So we have the passwords endpoints defined in the requests.js available.