Categories
Redux

Introduction to State Management with Redux

With Redux, we can use it to store data in a central location in our JavaScript app. It can work alone and it’s also a popular state management solution for React apps when combined with React-Redux.

In this article, we’ll look at how to create a basic store in the app and use it.

Installation

Redux is available as an NPM package. We can install it by running:

npm install --save redux

with NPM or:

yarn add redux

with Yarn.

Basic Example

We can use redux to store the state of an app by creating a store and reducer.

Then we can subscribe to the store to get the data and dispatch actions to make changes to the state.

For example, we can write:

import { createStore } from "redux";

function counter(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + action.payload || 1;
    case "DECREMENT":
      return state - action.payload || 1;
    default:
      return state;
  }
}

let store = createStore(counter);
store.subscribe(() => console.log(store.getState()));

store.dispatch({ type: "INCREMENT", payload: 2 });
store.dispatch({ type: "DECREMENT", payload: 1 });

In the code above, we have the counter reducer function which gets the action type and payload and then applies it accordingly.

The counter function checks for action type INCREMENT and DECREMENT and return the new state with the payload added to it.

Then we create the store by running:

let store = createStore(counter);

After that, we can subscribe to the store by calling subscribe on the store and then call getState within the callback for it as follows:

store.subscribe(() => console.log(store.getState()));

Then we can dispatch actions to the store by calling dispatch on it with an object with the type and payload , which will be passed into the counter function in the action parameter.

Therefore, the state will be updated by the action since for the first dispatch call:

store.dispatch({ type: "INCREMENT", payload: 2 });

The type will be passed in as action.type and payload will be set as action.payload to counter .

action.type is ‘INCREMENT’ and action.payload is 2.

Likewise, action.type is ‘DECREMENT’ and action.payload is 1 in the second call.

Note that we don’t mutate the state directly in counter . We return a new state by deriving it from the existing state.

Three Principles of Redux

Three principles apply to the way Redux is built.

Single Source of Truth

Redux stores state in a single place. This makes getting data easy since we only have to get it in one place.

Debugging is also easy because of that. This makes development faster.

Some functionality that was hard to implement now can be easy since now the state is stored in a single tree.

State is Read-Only

The state is read-only so it can’t be changed by accident. We have to express an intent to change the state.

There aren’t subtle race conditions to watch out for since all changes are centralized and happen one by one in strict order.

Since actions are just plain objects, they can be logged, serialized, stored and replayed for debugging and testing purposes.

Changes are Made With Pure Functions

Reducers are pure functions that take the previous state and action and return the next state. We return new state objects instead of mutation previous state.

We can start with a single reducer and then split it off into smaller reducers that management other parts of the state tree.

We can control how they’re called, pass additional data, or make reusable reducers.

Flux Architecture

Redux concentrates all model update logic in one layer of an app. This is one part of the Flux architecture that’s adopted by Redux.

Redux doesn’t have a dispatcher. It relies on pure functions instead of event emitters to change state.

Pure functions are easy to compose and doesn’t need extra management.

Redux assumes that we never mutate our data. We should always return new objects. This is easy now that we have the spread operator for objects.

Photo by Kelly Sikkema on Unsplash

RxJS

We can turn a Redux store into an Rxjs Observable by subscribing to it and calling nexr to return the latest state.

For example, we can write:

import { createStore } from "redux";

function counter(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + action.payload || 1;
    case "DECREMENT":
      return state - action.payload || 1;
    default:
      return state;
  }
}

let store = createStore(counter);

function toObservable(store) {
  return {
    subscribe({ next }) {
      const unsubscribe = store.subscribe(() => next(store.getState()));
      next(store.getState());
      return { unsubscribe };
    }
  };
}

const counterObservable = toObservable(store);

counterObservable.subscribe({
  next(count) {
    console.log(count);
  }
});

store.dispatch({ type: "INCREMENT", payload: 2 });
store.dispatch({ type: "DECREMENT", payload: 1 });

In the code above, we have:

function toObservable(store) {
  return {
    subscribe({ next }) {
      const unsubscribe = store.subscribe(() => next(store.getState()));
      next(store.getState());
      return { unsubscribe };
    }
  };
}

const counterObservable = toObservable(store);

counterObservable.subscribe({
  next(count) {
    console.log(count);
  }
});

store.dispatch({ type: "INCREMENT", payload: 2 });
store.dispatch({ type: "DECREMENT", payload: 1 });

The toObservable returns an Observable which we can subscribe to since it returns a subscribe method.

The Redux store’s subscribe method returns an unsubscribe function so we can unsubscribe to it when we no longer need it.

Then when we call dispatch above, we’ll get the new values logged in the next function above.

Conclusion

We can store an app’s data in a central place with Redux. To do this, we create a store and then subscribe to it to get the latest state from it.

Then we call dispatch on the store with a plain object with the action type and payload to change the store to the latest state.

Categories
MongoDB

Using MongoDB with Mongoose — Populate Virtuals and Count

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Populate Virtuals: The Count Option

We can add the count option when we create a populate virtual to get the count of the number of children we retrieved.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const PersonSchema = new Schema({
    name: String,
    band: String
  });

  const BandSchema = new Schema({
    name: String
  }, { toJSON: { virtuals: true } });

  BandSchema.virtual('numMembers', {
    ref: 'Person',
    localField: 'name',
    foreignField: 'band',
    count: true
  });

  const Person = db.model('Person', PersonSchema);
  const Band = db.model('Band', BandSchema);
  const person = new Person({ name: 'james', band: 'superband' });
  await person.save();
  const band = new Band({ name: 'superband' });
  await band.save();
  const doc = await Band.findOne({ name: 'superband' })
    .populate('numMembers');
  console.log(doc.numMembers);
}
run();

We created the PersonSchema and BandSchema schema objects.

We call the virtual method on the BandSchema schema with several options.

The ref property is the model that the Band model is referencing.

localField is the field in the BandSchema we want to join with PeronSchema .

foreignField is the field in the ForeignSchema we want to join with PersonSchema .

count set to true means that we get the count. numMembers is the field name that we get the count from.

Then we save a Person and Band document with the same name.

Then we call populate with the numMembers field to get the number of members.

And finally, we get the numMembers field from the retrieved result to get how many Person children entries are in the Band .

Populate in Middleware

We can add pre or post hooks to populate operations.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const PersonSchema = new Schema({
    name: String,
    band: String
  });

  const BandSchema = new Schema({
    name: String
  }, { toJSON: { virtuals: true } });

  BandSchema.virtual('numMembers', {
    ref: 'Person',
    localField: 'name',
    foreignField: 'band',
    count: true
  });

  BandSchema.pre('find', function () {
   this.populate('person');
  });

  BandSchema.post('find', async (docs) => {
    for (const doc of docs) {
      await doc.populate('person').execPopulate();
    }
  });

  const Person = db.model('Person', PersonSchema);
  const Band = db.model('Band', BandSchema);
  const person = new Person({ name: 'james', band: 'superband' });
  await person.save();
  const band = new Band({ name: 'superband' });
  await band.save();
  const doc = await Band.findOne({ name: 'superband' })
    .populate('numMembers');
  console.log(doc.numMembers);
}
run();

to add the pre and post hooks to listen to the find operation.

We should always call populate with the given field to do the population.

Conclusion

We can add the count property to add the count and also add pre and post hooks for find operations with when we do the population.

Categories
MongoDB

Using MongoDB with Mongoose — Populate Virtuals

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Populate Virtuals

We can control how 2 models are joined together.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const PersonSchema = new Schema({
    name: String,
    band: String
  });

  const BandSchema = new Schema({
    name: String
  });

  BandSchema.virtual('members', {
    ref: 'Person',
    localField: 'name',
    foreignField: 'band',
    justOne: false,
    options: { sort: { name: -1 }, limit: 5 }
  });

  const Person = db.model('Person', PersonSchema);
  const Band = db.model('Band', BandSchema);
  const person = new Person({ name: 'james', band: 'superband' });
  await person.save();
  const band = new Band({ name: 'superband' });
  await band.save();
  const bands = await Band.find({}).populate('members').exec();
  console.log(bands[0].members);
}
run();

We create the PersonSchema as usual, but the BandSchema is different.

We call the virtual method with the join field call members to get the persons with the band name set to a given name.

The ref property is the name of the model we want to join.

localField is the field of the BandSchema that we want to join with PersonSchema .

The foreignField is the field of PersonSchema that we want to join with the BandSchema .

justOne means we only return the first entry of the join.

options has the options for querying.

Virtuals aren’t included in the toJSON() output by default.

If we want populate virtual to show when using functions that rely on JSON.stringify() , then add the virtuals option and set it to true .

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const PersonSchema = new Schema({
    name: String,
    band: String
  });

  const BandSchema = new Schema({
    name: String
  }, { toJSON: { virtuals: true } });

  BandSchema.virtual('members', {
    ref: 'Person',
    localField: 'name',
    foreignField: 'band',
    justOne: false,
    options: { sort: { name: -1 }, limit: 5 }
  });

  const Person = db.model('Person', PersonSchema);
  const Band = db.model('Band', BandSchema);
  const person = new Person({ name: 'james', band: 'superband' });
  await person.save();
  const band = new Band({ name: 'superband' });
  await band.save();
  const bands = await Band.find({}).populate('members').exec();
  console.log(bands[0].members);
}
run();

to add the virtuals option to the BandSchema .

If we use populate projections, then foreignField should be included in the projection:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const PersonSchema = new Schema({
    name: String,
    band: String
  });

  const BandSchema = new Schema({
    name: String
  }, { toJSON: { virtuals: true } });

  BandSchema.virtual('members', {
    ref: 'Person',
    localField: 'name',
    foreignField: 'band',
    justOne: false,
    options: { sort: { name: -1 }, limit: 5 }
  });

  const Person = db.model('Person', PersonSchema);
  const Band = db.model('Band', BandSchema);
  const person = new Person({ name: 'james', band: 'superband' });
  await person.save();
  const band = new Band({ name: 'superband' });
  await band.save();
  const bands = await Band.find({}).populate({ path: 'members', select: 'name band' }).exec();
  console.log(bands[0].members);
}
run();

We call populate with an object with the path to get the virtual field and the select property has a string with the field names that we want to get separated by a space.

Conclusion

We can use Mongoose populate virtual feature to join 2 models by a column other than an ID column.

Categories
MongoDB

Using MongoDB with Mongoose — Discriminators

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Discriminators

Discriminators are a schema inheritance mechanism.

They let us enable multiple models to have overlapping schemas on top of the same MongoDB collection.

For example, we can use them as follows:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const options = { discriminatorKey: 'kind' };

  const eventSchema = new Schema({ time: Date }, options);
  const Event = db.model('Event', eventSchema);
  const ClickedLinkEvent = Event.discriminator('ClickedLink',
    new Schema({ url: String }, options));
  const genericEvent = new Event({ time: Date.now(), url: 'mongodb.com' });
  console.log(genericEvent)

  const clickedEvent =
    new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com' });
  console.log(clickedEvent)
}
run();

We created an Event model from the eventSchema .

It has the discriminatorKey so that we get can discriminate between the 2 documents we create later.

To create the ClickedLinkEvent model, we call Event.discriminator to create a model by inheriting from the Event schema.

We add the url field to the ClickedLinkEvent model.

Then when we add the url to the Event document and the ClickedLinkEvent document, only the clickedEvent object has the url property.

We get:

{ _id: 5f6f78f17f83ca22408eb627, time: 2020-09-26T17:22:57.690Z }

as the value of genericEvent and:

{
  _id: 5f6f78f17f83ca22408eb628,
  kind: 'ClickedLink',
  time: 2020-09-26T17:22:57.697Z,
  url: 'mongodb.com'
}

as the value of clickedEvent .

Discriminators Save to the Model’s Collection

We can save different kinds of events all at once.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const options = { discriminatorKey: 'kind' };

  const eventSchema = new Schema({ time: Date }, options);
  const Event = db.model('Event', eventSchema);

  const ClickedLinkEvent = Event.discriminator('ClickedLink',
    new Schema({ url: String }, options));

  const SignedUpEvent = Event.discriminator('SignedUp',
    new Schema({ user: String }, options));

  const event1 = new Event({ time: Date.now() });
  const event2 = new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com' });
  const event3 = new SignedUpEvent({ time: Date.now(), user: 'mongodbuser' });
  await Promise.all([event1.save(), event2.save(), event3.save()]);
  const count = await Event.countDocuments();
  console.log(count);
}
run();

We created the eventSchema as an ordinary schema.

And the rest of the models are created from the Event.discriminator method.

Then we created the models and saved them all.

And finally, we called Event.countDocuments to get the number of items saved under the Event model.

Then count should be 3 since ClickedLinkEvent and SignedUpEvent both inherit from Event itself.

Discriminator Keys

We can tell the difference between each type of models with the __t property by default.

For instance, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');

  const eventSchema = new Schema({ time: Date });
  const Event = db.model('Event', eventSchema);

  const ClickedLinkEvent = Event.discriminator('ClickedLink',
    new Schema({ url: String }));

  const SignedUpEvent = Event.discriminator('SignedUp',
    new Schema({ user: String }));

  const event1 = new Event({ time: Date.now() });
  const event2 = new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com' });
  const event3 = new SignedUpEvent({ time: Date.now(), user: 'mongodbuser' });
  await Promise.all([event1.save(), event2.save(), event3.save()]);
  console.log(event1.__t);
  console.log(event2.__t);
  console.log(event3.__t);
}
run();

to get the type of data that’s saved from the console logs. We should get:

undefined
ClickedLink
SignedUp

logged.

We can add the discriminatorKey in the options to change the discriminator key.

So we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const options = { discriminatorKey: 'kind' };
  const eventSchema = new Schema({ time: Date }, options);
  const Event = db.model('Event', eventSchema);

const ClickedLinkEvent = Event.discriminator('ClickedLink',
    new Schema({ url: String }, options));

const SignedUpEvent = Event.discriminator('SignedUp',
    new Schema({ user: String }, options));

const event1 = new Event({ time: Date.now() });
  const event2 = new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com' });
  const event3 = new SignedUpEvent({ time: Date.now(), user: 'mongodbuser' });
  await Promise.all([event1.save(), event2.save(), event3.save()]);
  console.log(event1.kind);
  console.log(event2.kind);
  console.log(event3.kind);
}
run();

to set the options for each model and then access the kind property instead of __t and get the same result as before.

Conclusion

We can use discriminators to create new models by inheriting one model from another.

Categories
MongoDB

Using MongoDB with Mongoose — Discriminators and Hooks

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Discriminators and Queries

Queries are smart enough to take into account discriminators.

For example, if we have:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const options = { discriminatorKey: 'kind' };
  const eventSchema = new Schema({ time: Date }, options);
  const Event = db.model('Event', eventSchema);

  const ClickedLinkEvent = Event.discriminator('ClickedLink',
    new Schema({ url: String }, options));

  const SignedUpEvent = Event.discriminator('SignedUp',
    new Schema({ user: String }, options));

  const event1 = new Event({ time: Date.now() });
  const event2 = new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com' });
  const event3 = new SignedUpEvent({ time: Date.now(), user: 'mongodbuser' });
  await Promise.all([event1.save(), event2.save(), event3.save()]);
  const clickedLinkEvent =  await ClickedLinkEvent.find({});
  console.log(clickedLinkEvent);
}
run();

We called the ClickedLinkEvent.find method.

Therefore, we’ll get all the ClickedLinkEvent instances.

Discriminators Pre and Post Hooks

We can add pre and post hooks to schemas created with discriminators.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const options = { discriminatorKey: 'kind' };
  const eventSchema = new Schema({ time: Date }, options);
  const Event = db.model('Event', eventSchema);

  const clickedLinkSchema = new Schema({ url: String }, options)
  clickedLinkSchema.pre('validate', (next) => {
    console.log('validate click link');
    next();
  });
  const ClickedLinkEvent = Event.discriminator('ClickedLink',
    clickedLinkSchema);

  const event1 = new Event({ time: Date.now() });
  const event2 = new ClickedLinkEvent({ time: Date.now(), url: 'mongodb.com' });
  await event2.validate();
}
run();

to add a pre hook for the validate operation to the clickedLinkSchema .

Handling Custom _id Fields

If an _id field is set on the base schema, then it’ll always override the discriminator’s _id field.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const options = { discriminatorKey: 'kind' };

  const eventSchema = new Schema({ _id: String, time: Date },
    options);
  const Event = db.model('BaseEvent', eventSchema);

  const clickedLinkSchema = new Schema({
    url: String,
    time: String
  }, options);

  const ClickedLinkEvent = Event.discriminator('ChildEventBad',
    clickedLinkSchema);

  const event1 = new ClickedLinkEvent({ _id: 'custom id', time: '12am' });
  console.log(typeof event1._id);
  console.log(typeof event1.time);
}
run();

And from the console log, we can see that both the _id and time fields of event1 are strings.

So the _id field is the same one as the eventSchema , but the ClickedLinkEvent field has the same type as the one in clickedLinkSchema .

Using Discriminators with Model.create()

We can use discriminators with the Model.create method.

For example, we can write:

async function run() {
  const { createConnection, Types, Schema } = require('mongoose');
  const db = createConnection('mongodb://localhost:27017/test');
  const shapeSchema = new Schema({
    name: String
  }, { discriminatorKey: 'kind' });

  const Shape = db.model('Shape', shapeSchema);

  const Circle = Shape.discriminator('Circle',
    new Schema({ radius: Number }));
  const Square = Shape.discriminator('Square',
    new Schema({ side: Number }));

  const shapes = [
    { name: 'Test' },
    { kind: 'Circle', radius: 5 },
    { kind: 'Square', side: 10 }
  ];
  const [shape1, shape2, shape3] = await Shape.create(shapes);
  console.log(shape1 instanceof Shape);
  console.log(shape2 instanceof Circle);
  console.log(shape3 instanceof Square);
}
run();

We created 3 schemas for shapes with the discriminator method.

Then we called Shape.create with an array of different shape objects.

We specified the type with the kind property since we set that as the discriminator key.

Then in the console log, they should all log true since we specified the type of each entry.

If it’s not specified, then it has the base type.

Conclusion

We can add hooks to schemas created from discriminators.

_id fields are handled differently from other discriminator fields.

And we can use the create method with discriminators.