React Material Dynamically Dependent Dropdown with NodeJS API & SQLite
Table of Contents
Overview
This article is a how-to for getting the data for an Autocomplete field which populates a Dropdown field with an API call. The example uses a vehicle makes (autocomplete) and models (dropdown) combination to select a specific automobile. The API is Node Express and uses a SQLite database to keep it light and simple. There are about 20 makes and the models have years but they will be filtered out with using Distinct in the select statements. When a Make is selected a small spinner will show in the Models dropdown field to indicate the field is being populated with new data.

Download Source from GitHub https://github.com/fullstacksoup
GitHubPart I – React Client Side App
Prerequisites
You will need to create and install the following libraries for this example. I recommend getting the source from GitHub unless you want to try it step by step.
Create a New React-App
npx create react-app react-app
Material UI – Version 5
NOTE: React-Material Version 5 was was released in October of 2021.
For the documentation click here
npm install @emotion/react @emotion/styled @mui/icons-material @mui/material
Axios
Axios is a promise-based HTTP Client.
npm i axios
Autocomplete and Dynamic Drop Down Field
In some cases the first drop down may have hundreds or thousands of options therefore an autocomplete is an ideal control. The second field will have about 4-50 options to choose from and is more suitable for a dropdown field.
When an option is chosen from the autocomplete then an API call to get the models by MakeId will be triggered. The response then fills the dropdown field with a list of models.
Component – Source Code
import React, { useEffect, useState } from 'react';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import Container from '@mui/material/Container';
import { useTheme } from '@mui/material/styles';
import OutlinedInput from '@mui/material/OutlinedInput';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import axios from 'axios';
import CircularProgress from '@mui/material/CircularProgress';
import InputAdornment from '@mui/material/InputAdornment';
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250,
},
},
};
function getStyles(name, personName, theme) {
return {
fontWeight:
personName.indexOf(name) === -1
? theme.typography.fontWeightRegular
: theme.typography.fontWeightMedium,
};
}
export default function CustomizedSelects() {
const theme = useTheme();
const [isLoading, setIsLoading] = useState(false);
const [option, setOption] = useState([]);
const [make, setMake] = useState('');
const [model, setModel] = useState('');
const [makeData, setMakeData] = useState([]);
const [modelData, setModelData] = useState([{Model: 'Please Select Make'}]);
const [value, setValue] = useState('');
useEffect(() => {
const url = '/api/makes';
axiosConn.get(url)
.then(function (innerResponse) {
setMakeData(innerResponse.data.data);
console.log(innerResponse);
}).catch(function (innerResponse) {
console.log(innerResponse);
})
}, []);
const handleChange = (event) => {
const {
target: { value },
} = event;
setModel(value);
};
const escapeRegexCharacters = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const getSuggestions = val => {
const escapedValue = escapeRegexCharacters(val.trim());
if (escapedValue === '') {
return [];
}
const regex = new RegExp('^' + escapedValue, 'i');
return makeData.filter(makeData => makeData.Make);
}
const onSelectVehicleMakeOption = (option, newValue) => {
setIsLoading(true);
if(newValue != null) {
const url = `/api/models/${newValue.MakeId}`
axios.get(url)
.then(function (results) {
setModelData(results.data.data);
console.log(results);
setTimeout(() => {
setIsLoading(false);
}, 1000);
}).catch(function (innerResponse) {
console.log(innerResponse);
})
setMake(newValue.Make);
} else {
setMake([]);
}
}
return (
<Container maxWidth="md" style={{marginTop: '35px'}}>
<FormControl sx={{ m: 1 }} variant="standard">
<Autocomplete
disableClearable
options={makeData}
onChange={(event, newValue) => onSelectVehicleMakeOption(option, newValue)}
getOptionLabel={(option) => option.Make}
defaultValue={getSuggestions(value)[0]}
sx={{ width: 300 }}
renderInput= {(params) => {
console.log('PARAMS', params);
return (
<TextField
{...params}
label="Vehicle Make"
variant="outlined"
style={{lineHeight: '28px'}}
onChange={(e) => setValue(e.target.value)}
placeholder="Enter Make"
autoComplete="off"
size={'small'}
InputProps={{
...params.InputProps,
}}
/>
);
}}
/>
</FormControl>
<FormControl sx={{ m: 1, width: 300 }} size={'small'}>
<InputLabel id="demo-multiple-name-label">Vehicle Model</InputLabel>
<Select
labelId="demo-multiple-name-label"
id="demo-multiple-name"
value={model}
onChange={handleChange}
input={<OutlinedInput label="Vehicle Model" />}
startAdornment={
<InputAdornment position="start">
{isLoading === true?
<CircularProgress size={15}/>
:
''
}
</InputAdornment>
}
MenuProps={MenuProps}
>
{modelData.map((name) => (
<MenuItem
key={name.Model}
value={name.Model}
>
{name.Model}
</MenuItem>
))}
</Select>
</FormControl>
</Container>
);
}
Breakdown of React Component
Fill in the Make Autocomplete
Initialize the Make data array with an API call to get the Makes. Also initialize the Models Array with the data from SQLite.
const [makeData, setMakeData] = useState([]);
const [modelData, setModelData] = useState([{Model: 'Please Select Make'}]);
useEffect(() => {
const url = '/api/makes';
axios.get(url)
.then(function (innerResponse) {
setMakeData(innerResponse.data.data);
console.log(innerResponse);
}).catch(function (innerResponse) {
console.log(innerResponse);
})
}, []);
The Autocomplete Field – Makes & Make ID
Filter out the escape characters for the material autocomplete. This example is taken from Material UI’s MUI5 page for autocomplete examples here. The disableClearable option is used to prevent clearing out the field. getSuggestions() is how the Make array is filtered when a user types in the autocomplete field,
const escapeRegexCharacters = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const getSuggestions = val => {
const escapedValue = escapeRegexCharacters(val.trim());
if (escapedValue === '') {
return [];
}
const regex = new RegExp('^' + escapedValue, 'i');
return makeData.filter(makeData => makeData.Make);
}
...
<FormControl sx={{ m: 1 }} variant="standard">
<Autocomplete
disableClearable
options={makeData}
onChange={(event, newValue) => onSelectVehicleMakeOption(option, newValue)}
getOptionLabel={(option) => option.Make}
defaultValue={getSuggestions(value)[0]}
sx={{ width: 300 }}
renderInput= {(params) => {
console.log('PARAMS', params);
return (
<TextField
{...params}
label="Vehicle Make"
variant="outlined"
style={{lineHeight: '28px'}}
onChange={(e) => setValue(e.target.value)}
placeholder=""
autoComplete="off"
size={'small'}
InputProps={{
...params.InputProps,
}}
/>
);
}}
/>
</FormControl>
Dropdown Field – Models
The dropdown field is populated when an option is selected by the autocomplete field. Using the Make ID the API is called to get the specific data set for Models with the Make ID that is sent via Get call onSelectVehicleMakeOption(),
Highly recommend adding a spinner as an adornment in the Dropdown Field so user know there is data being loaded in the field, The isLoad boolean variable is set to 1000ms = 1 second otherwise it won’t display the spinner in the input adornment.
const [model, setModel] = useState('');
...
const handleModelChange = (event) => {
const {
target: { value },
} = event;
setModel(value);
};
const onSelectVehicleMakeOption = (option, newValue) => {
const url = `/api/models/${newValue.MakeId}`
axios.get(url)
.then(function (results) {
setModelData(results.data.data);
setTimeout(() => {
setIsLoading(false);
}, 1000);
}).catch(function (innerResponse) {
console.log(innerResponse);
})
setMake(newValue.Make);
}
...
<FormControl sx={{ m: 1, width: 300 }} size={'small'}>
<InputLabel id="demo-multiple-name-label">Vehicle Model</InputLabel>
<Select
labelId="demo-multiple-name-label"
id="demo-multiple-name"
value={model}
onChange={handleChange}
input={<OutlinedInput label="Vehicle Model" />}
MenuProps={MenuProps}
startAdornment={
<InputAdornment position="start">
{isLoading === true?
<CircularProgress size={15}/>
:
''
}
</InputAdornment>
}
>
{modelData.map((name) => (
<MenuItem
key={name.Model}
value={name.Model}
>
{name.Model}
</MenuItem>
))}
</Select>
</FormControl>
Part II – NodeJS API
Prerequisites
If you don’t already have Node.js installed then you can get the download and install information from their website at https://nodejs.org/.
Install Express
Create a folder for your project and go into that directory and run the following commands.
npm init
npm install express
Install Nodemon
Nodemon restarts the node.js application when changes are made.
npm install nodemon
Update the “scripts” section of the “package.json” file with the following:
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"start": "nodemon app.js"
},
To start the application use npm start instead of node app.js. This enables hot reloads so you don’t have to restart your API every time it is updated.
Install the SQL Lite Driver.
npm install sqlite3
Handling CORS
Cross Origin Resource Sharing is needed if you want to use the API with an external application.
npm i cors
Code for the API
The SQLite database is already populated with a table labeled VehicleMakeModel_LU. This table has about thousand records and it includes the various years which will not be needed for this example but could be a good challenge for those of you that want to take this example one step further,
The API has two get methods, one for all the Makes and the another for the Models by Make ID.
app.js Source Code
const express = require('express');
const app = express();
const port = 44410;
const cors = require('cors');
var sqlite3 = require("sqlite3"),
TransactionDatabase = require("sqlite3-transactions").TransactionDatabase;
const DBSOURCE = "vehicledb.sqlite";
var db = new TransactionDatabase(
new sqlite3.Database(DBSOURCE, (err) => {
if (err) {
// Cannot open database
console.error(err.message)
throw err
}
else {
}
})
);
module.exports = db
app.use(
express.urlencoded(),
cors()
);
app.get('/', (req, res) => res.send('API Root'));
app.get("/api/makes", async (req, res, next) => {
try {
var sql = "SELECT DISTINCT MakeId, Make FROM VehicleMakeModel_LU"
var params = []
db.all(sql, params, (err, rows) => {
if (err) {
res.status(400).json({"error":err.message});
return;
}
res.json({
"message":"success",
"data":rows
})
});
} catch (err) {
console.log(err);
}
});
app.get("/api/models/:id", async (req, res, next) => {
var params = []
try {
var sql = "SELECT DISTINCT MakeId, Make, Model FROM VehicleMakeModel_LU WHERE MakeId = ?"
var params = [req.params.id]
db.all(sql, params, (err, rows) => {
if (err) {
res.status(400).json({"error":err.message});
return;
}
res.json({
"message":"success",
"data":rows
})
});
} catch (err) {
console.log(err);
}
});
app.listen(port, () => console.log(`API listening on port ${port}!`));
Running the Example.
Run npm start for the NodeJS API and the React App from the command line or in a VS Code terminal.

You must be logged in to post a comment.