I am learning how to use components in React. I have a parent called Quote.jsx that has a child called QuoteData.jsx. I send the value of selectedCharacters from the parent to the child. On the child I am using a react-select control. The react-select control’s value is set to selectedCharacters.
I know selectedCharacters only has 2 elements in it because I can see that on the screen via the temporary output message I put in there that checks the length of selectedCharacters and shows the values in those 2 elements. But the react-select element shows an extra blank element and I cannot figure out why.
Since a picture’s worth a thousand, this is what I mean

And it’s not only a single element. I mean it is when I first load the page but as I use the navigation buttons, extra blank items are showing in the react-select.
My jsx files look like this:
Quote.jsx
import React, { useState, useEffect } from 'react';
//import Select from 'react-select';
import axios from 'axios';
import { ToastContainer, toast } from 'react-toastify';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'react-toastify/dist/ReactToastify.css';
import './App.css';
import Processing from './Processing';
import QuoteData from './QuoteData';
function Quote() {
const [quote, setQuote] = useState({ Text: '', Id: 0, Formatted: '', IsActive: true, Characters: [] });
const [allCharacters, setAllCharacters] = useState([]);
const [selectedCharacters, setSelectedCharacters] = useState([]);
const [allQuoteIds, setAllQuoteIds] = useState([]);
const [currentQuoteIdIndex, setCurrentQuoteIdIndex] = useState(0);
const [selectedOption, setSelectedOption] = useState('optAll');
const [statusCount, setStatusCount] = useState({formatted_count: 0, non_formatted_count: 0, total_count: 0});
const [loadingIndicator, setLoadingIndicator] = useState(true)
// ---------------------- HOOKS ----------------------
useEffect(() => {
try {
getAllQuoteIds();
getCharacterData();
getStatusCount();
} catch (error) {
showToast('error', 'Error gathering initial data. Error reported is ' + error.message)
}
}, []);
useEffect(() => {
try {
getQuoteData();
setSelectedCharacters(quote.Characters)
} catch (error) {
showToast('error', 'Problem getting/setting quote data' + error.message)
}
}, [allQuoteIds, currentQuoteIdIndex]);
// ---------------------- HELPERS ----------------------
const showToast = (type, message) => {
const toastOptions = {
position: "top-center",
autoClose: 500,
hideProgressBar: false,
closeOnClick: true,
draggable: true,
progress:undefined,
}
if (type === "warning") {
toast.warning(message, {toastOptions});
} else if (type === "success") {
toast.success(message, {toastOptions});
} else if (type === "error") {
toast.error(message, {toastOptions})
} else if (type === "info") {
toast.info(message, {toastOptions})
} else {
toast.error('Unknown issue occurred when trying to display status', {toastOptions})
}
}
const getCharacterData = async () => {
try {
loadCharacters(false);
} catch (error) {
showToast('error', 'Problem getting/loading character data. Problem reported: ' + error.message);
}
};
// ---------------------- GET CALLS ----------------------
const getQuoteData = async () => {
if(allQuoteIds[currentQuoteIdIndex] === undefined) {
// do nothing
} else {
try {
let url = 'http://127.0.0.1:5000/AdminAPI/getOneQuote/' + allQuoteIds[currentQuoteIdIndex]
const response = await axios.get(
url,
{
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
}
);
if (response.status === 200) {
setLoadingIndicator(false);
setQuote(response.data.quotes[0]);
setSelectedCharacters(response.data.quotes[0].Characters.map((character) => ({
value: character.Id,
label: character.ShortName,
})));
} else {
showToast('error', 'Problem getting quote data');
}
} catch (error) {
showToast('error', 'Problem getting quote data. Problem reported: ' + error.message);
}
}
};
const getAllQuoteIds = async (whatToShow = 'optAll') => {
try {
let urlBase = 'http://127.0.0.1:5000/AdminAPI/getAllQuoteIds';
let url = '';
if (whatToShow === "optFormatted") {
url = urlBase + '?formatted=true';
}
else if (whatToShow === "optNonFormatted") {
url = urlBase + '?formatted=false';
}
else {
url = urlBase;
}
const response = await axios.get(
url,
{
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
}
);
setAllQuoteIds(response.data);
} catch (error) {
showToast('error', 'Problem getting quoteID data. Problem reported: ' + error.message);
}
}
const getStatusCount = async() => {
try {
const response = await axios.get(
'http://127.0.0.1:5000/AdminAPI/GetQuoteFormattingStatusCount',
{
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
}
);
setStatusCount(response.data)
} catch (error) {
showToast('error', 'Problem getting status count data. Problem reported: ' + error.message);
}
}
// ---------------------- POST CALLS ----------------------
const saveQuote = async () => {
try {
const quoteData = {
Id: quote.Id,
Text: quote.Text,
Characters: selectedCharacters.map((character) => character.value),
Formatted: true,
IsActive: quote.IsActive
};
try {
const response = await axios.post('http://127.0.0.1:5000/AdminAPI/saveQuote', quoteData, {
headers: {
'Content-Type': 'application/json',
},
});
// Handle successful response
if (response.status === 200) {
showToast("success", "Quote saved successfully!")
getStatusCount();
handleNavigationChange({ target: {value: "next"}});
} else {
showToast("error", "Error saving quote")
// toast.error('Error saving quote');
//TODO: Put in logging
}
} catch (error) {
showToast("error", "Error saving quote")
// toast.error('Error saving quote');
//TODO: Put in logging
}
} catch (error) {
showToast('error', 'Problem saving quote data. Problem reported: ' + error.message);
}
};
// ---------------------- EVENT HANDLING ----------------------
const handleNavigationChange = (event) => {
if (event.target.value === "start") {
setCurrentQuoteIdIndex(0);
} else if (event.target.value === "prev"){
if (currentQuoteIdIndex === 0){
showToast("warning", "Cannot move backward any further")
} else {
setCurrentQuoteIdIndex(currentQuoteIdIndex-1);
}
} else if (event.target.value === "next"){
if (currentQuoteIdIndex >= allQuoteIds.length-1) {
showToast("warning", "Cannot move forward any further")
} else {
setCurrentQuoteIdIndex(currentQuoteIdIndex+1);
}
} else {
setCurrentQuoteIdIndex(allQuoteIds.length-1)
}
}
const handleQuoteChange = (event) => {
setQuote({ ...quote, Text: event.target.value });
};
const handleCharChange = (selected) => {
setSelectedCharacters(selected);
setQuote({...quote, Characters: selected });
};
const handleOptionChange = (event) => {
setLoadingIndicator(true);
setSelectedOption(event.target.value);
getAllQuoteIds(event.target.value);
};
const handleIsActiveChange = (event) => {
setQuote({ ...quote, IsActive: event.target.checked });
}
const handleClickReloadCharacters = (event) => {
loadCharacters(true);
};
const loadCharacters = async (showSuccessMessage) => {
try {
const response = await axios.get(
'http://127.0.0.1:5000/AdminAPI/getAllCharacters',
{
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
}
);
const formattedCharacters = response.data.characters.map((character) => ({
value: character.Id,
label: character.ShortName,
}));
setAllCharacters(formattedCharacters);
if (showSuccessMessage) {
showToast('info', 'Characters loaded')
}
} catch (error) {
showToast('error', 'Problem getting/loading character data. Problem reported: ' + error.message);
}
}
// ---------------------- PAGE ----------------------
return (
<div>
<ToastContainer/>
<br/>
<div className='container'>
<div className="row">
<div className="col-md-12">
<select value={selectedOption} onChange={handleOptionChange}>
<option value="optAll">Show All Quotes</option>
<option value="optFormatted">Only Show Formatted Quotes</option>
<option value="optNonFormatted">Only Show Non-Formatted Quotes</option>
</select>
</div>
</div>
</div>
<div style={{height: "10%"}}> </div>
{
loadingIndicator === false ? (
<QuoteData
quote={quote}
onCharChange={handleCharChange}
onIsActiveChange={handleIsActiveChange}
onQuoteChange={handleQuoteChange}
allCharacters={allCharacters}
selectedCharacters={selectedCharacters}
>
</QuoteData>
) : (
<div className="container-fluid d-flex align-items-center justify-content-center"
style={{padding: "20px", height: "50vh", width: "50vw"}}>
<Processing></Processing>
</div>
)}
<button className='btn btn-primary' onClick={saveQuote}>Save Quote</button>
<br/><br/>
<button className='btn btn-primary' value="start" onClick={handleNavigationChange}>Start</button>
<button className='btn btn-primary' value="prev" onClick={handleNavigationChange}>Prev</button>
<button className='btn btn-primary' value="next" onClick={handleNavigationChange}>Next</button>
<button className='btn btn-primary' value="end" onClick={handleNavigationChange}>End</button>
<br/><br/>
<button className='btn btn-primary' value="reloadCharacters" onClick={handleClickReloadCharacters}>Reload Characters</button>
<br/><br/>
<div className="container-fluid d-flex align-items-center justify-content-center">
Formatted: {statusCount.formatted_count} of {statusCount.total_count}
<div className="progress" style={{height: "40px", width: "50%"}} >
<div className="progress-bar bg-success" style={{width: ((statusCount.formatted_count/statusCount.total_count)*100).toString() + "%"}} >
{ Math.fround((statusCount.formatted_count/statusCount.total_count)*100).toFixed(2)}%
</div>
<div className="progress-bar bg-danger" style={{width: ((statusCount.non_formatted_count/statusCount.total_count)*100).toString() + "%"}} >
{ Math.fround((statusCount.non_formatted_count/statusCount.total_count)*100).toFixed(2)}%
</div>
</div>
</div>
</div>
);
}
export default Quote;
QuoteData.jsx
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import Select from 'react-select';
function QuoteData ({quote, allCharacters, selectedCharacters, onIsActiveChange, onQuoteChange, onCharChange}) {
const handleCharChange = (selected) => {
const formattedSelected = selected || [];
onCharChange(formattedSelected);
};
return (
<div>
<div className='container'>
<div className='row'>
<div className='col text-start'>
{quote.Book.Title}
</div>
<div className="col text-end">
Quote ID: {quote.Id}
<text value={quote.Id} type="hidden"></text>
</div>
</div>
<div className='row'>
<div className='col-lg'>
<textarea className="form-control border border-primary"
style={{ fontSize: '18px'}}
value={quote.Text} rows="17"
onChange={onQuoteChange}
disabled={!quote.IsActive}
/>
{/* For testing only */}
SelectedCharacters length is {selectedCharacters.length}
<br></br>
{selectedCharacters && selectedCharacters.length > 0 ? (
<div>
<ul>
{selectedCharacters.map((character) => (
<li key={character.value}>{character.label}</li> // Display ShortName in a list item
))}
</ul>
</div>
) : (
<div>No characters selected</div>
)}
{/* end for testing */}
<div>
<Select
isMulti
options = {
allCharacters
}
value={selectedCharacters}
onChange={handleCharChange}
>
</Select>
</div>
<input type="checkbox" id="chkIsActive" checked={quote.IsActive} onChange={onIsActiveChange} />
<label htmlFor="chkIsActive"> Is Active</label>
</div>
</div>
</div>
</div>
)
}
export default QuoteData
I would greatly appreciate it if you could help me understand where this (these after navigation) extra element(s) are coming from. Thanks!