Recently I had to create an autocomplete component for a React project, the difference here is that even though the data was coming from the popular NoSQL datastore MongoDB I had to pull it from several sources.

With Mongo and Node.JS API calls you only really have one hit you can use in order to request all the data you need for the request.

Now you can use Async with Mongoose to recall several tables at once:

Server: With Async

var team = require('../../models/team.js');
var user = require('../../models/user.js');
var async = require('async');

try {
  async.parallel({
    teams: function(cb){
      team.find({}, '_id title')
      .lean() // cut out virtuals
      .exec(cb)
    },
    users: function(cb){
      user.find({}, '_id email')
      .lean() // cut out virtuals
      .exec(cb)
    }
  }, function(err, results){

    const teams = results.teams // store teams data
    const users = results.users // store users data

    // and then process the results

But for autocomplete it seems impractical to use, so we'll be using Mongoose's aggregate feature link and return just the data we need in the format we want.

Client: AutoComplete Component

First we'll create a React component to autocomplete team names, we'll use the isomorphic-fetch component to trigger the call. Using React-Select to render the results and using props to pass back and forth the selected data to the parent.

Here the autocomplete component returns all teams with their assigned user.

import React, { Component } from 'react';
import Select from 'react-select';
import fetch from 'isomorphic-fetch';

class AutoCompleteTeams extends Component {
  // define initial props
  static defaultProps = {
    className: '',
    update: null,
    initialValue: { _id: '', title: '' }
  }

  // define initial state
  constructor(props) {
    super(props);
    this.state = {
      selectedArray: new Object()
    }
  }

  componentWillReceiveProps(newProps){
    if (newProps === this.props) {
      this.setState({
        selectedArray: new Object()
      }, () => {
        return;
      })
    }

    this.setState({
      selectedArray: newProps.initialValue || new Object()
    }, function() {
    })
  }

  // onChange (item selected), return value to parent
  // => { title: 'Alexandria (john@smith.com)' }
  onChange = (value) => {
		this.setState({
      selectedArray: value,
		}, function() {
      this.props.update(value);
    });
	}

  // on autocomplete, perform GET request. 
  // We'll also send credentials as we're operating
  // within a secure session
  autocomplete = (input) => {
    if (!input) {
			return Promise.resolve({ options: [] });
    }

    return fetch(`/v1/users/autocomplete/teams?q=${input}`, {
      method: 'GET',
      credentials: 'include' })
      .then((response) => response.json())
      .then((json) => {
        return { options: json };
      });
  }

  // render autocomplete using Select.Async displaying
  render() {
    const { className} = this.props
    return (
       <div className={`form-control ${className}`}>
         <Select.Async
           value={this.state.selectedArray}
           onChange={this.onChange}
           valueKey="_id" // define which value to use when item selected
           labelKey="title" // define which field to display in select
           loadOptions={this.autocomplete}
           backspaceRemove={true}
         />
       </div>
    )
  }
}

export default AutoCompleteTeams;

Client: Parent Usage

We can then add it to our parent component via:

import React, { Component } from 'react';
import AutoCompleteTeams from './../sharedComponents/AutoCompleteTeams'

class AddTeamLeader extends Component {

  setTeam = (value) => {
    this.setState({ user: value })
  }

  render() {
    return (<div>
      <AutoCompleteTeams
        className="col-xs-12 col-sm-6 col-md-8"
        name="title"
        id="title"
        initialValue={this.state.team}
        update={this.setTeam}
        />
      </div>)
  }
}

Server: Aggregate Query

Now we'll define the get request, using aggregation to return a list of teams with their respective leader's email in brackets.

var team = require('../../models/team.js');

exports.autocompleteTeams = function(req, res) {
  const q = req.query.q || ''
  if (q) {
    team.aggregate([
      // Order is important, each extra match drills down the results available.
      {$lookup: {from: 'users', localField: 'leader', foreignField: '_id', as: 'leader'} },
      {$project: {"title" :
        { $concat : [
          "$title",
          " (",
          { $arrayElemAt:["$leader.email", 0] },
          ")"
        ] }
      }},
      {$match: {"title": { "$regex": q, "$options": "i" }}},
      {$sort: {"title": 1}}
    ])
    .limit(25)
    .then((records) => res.send(records))
  } else {
    team.aggregate([
      {$lookup: {from: 'users', localField: 'leader', foreignField: '_id', as: 'leader'} },
      {$project: {"title" :
        { $concat : [
          "$title",
          " (",
          { $arrayElemAt:["$leader.email", 0] },
          ")"
        ] }
      }},
      {$sort: {"title": 1}}
    ])
    .limit(25)
    .then((records) => res.send(records))
  }
}

Lets drill down the aggregate action, as we cannot access virtuals inside mongoose queries we cannot pull this data from a populate() action and then pass it into our records in one so we do a look up connecting team to users via it's assigned 'leader' field which will match the user field _id.

{$lookup: {from: 'users', localField: 'leader', foreignField: '_id', as: 'leader'} },

Next with the user connected to team we project or create the title value concatenating the email address to the end in brackets.

{$project: {"title" :
  { $concat : [
    "$title",
    " (",
    { $arrayElemAt:["$leader.email", 0] },
    ")"
  ] }
}},

Then we match the result to the API request value.

{$match: {"title": { "$regex": q, "$options": "i" }}},

Sort the results.

{$sort: {"title": 1}}

Giving us the end output: