I'm trying to build a dynamic form with react which is auto populated with components when received data from api, here I'm able to display and render the components which are the n numbers of dropdowns and radio buttons on my form basis on what I'll be receiving from the api response, but still I want to pass the data selected from the from on onSubmit() function. I'm facing certain challenges in collecting the data altogether, as I'm using rfce, I can get data of single selection whereas I want a collective selection data.
Below attached is my code.
This is the main card component. Which is responsible for getting api and passing props to sub components that are dropdowns and radio button based on their types
import React, { useEffect, useState } from "react";
import { Card, Button, FloatingLabel, Form } from "react-bootstrap";
import "bootstrap/dist/css/bootstrap.min.css";
import {
SurveyDropdown,
SurveyRadioButton,
} from "./subcomponents/SurveyComponents";
import { getQuestions } from "../util/http";
const SurveyCard = () => {
const [surveyData, setSurveyData] = useState([]);
useEffect(() => {
// Update the document title using the browser API
getQuestions()
.then((resp) => {
setSurveyData(resp.data);
})
.catch((err) => {
console.log(err);
if (
!err.response ||
!err.response.data ||
!err.response.data.errorCode
) {
console.log("server failed to response");
return;
}
switch (err.response.data.errorCode) {
case 50: // Survey submitted
window.location.replace("/surveysubmitted");
break;
default:
console.error("Error code not registered ");
break;
}
});
}, []);
return (
<>
<Card>
<Card.Header></Card.Header>
<Card.Body>
<Card.Title>Please provide your feedback</Card.Title>
{surveyData.map((question) => {
if (question.type === "dropdown") {
return <SurveyDropdown key={question.id} value={question} />;
} else if (question.type === "button") {
return <SurveyRadioButton value={question} />;
}
})}
<FloatingLabel
controlId="floatingTextarea2"
label="Comments"
className="for-spacing"
>
<Form.Control
as="textarea"
placeholder="Leave a comment here"
style={{ height: "100px" }}
/>
</FloatingLabel>{" "}
<br />
<Button variant="primary">Submit</Button>
</Card.Body>
</Card>
<br />
<br />
</>
);
};
export default SurveyCard;
This is component which is rendering all the dropdowns and radio buttons. Here I'm trying to get the collective data from all displayed dropdowns and radio button, but instead am getting only one state a time.
import React, { useState } from "react";
import { Form, FloatingLabel } from "react-bootstrap";
export const SurveyDropdown = (props) => {
const [selectedOption, setSelectedOption] = useState();
console.log("Selected Option", selectedOption);
function handleSelectChange(event) {
setSelectedOption(event.target.value);
}
return (
<React.Fragment>
<FloatingLabel
className="for-spacing"
controlId="floatingSelect"
label={props.value.surveyQuestion}
>
<Form.Select onChange={handleSelectChange}>
{props.value.option.map((surveyD, index) => (
<option
value={surveyD}
key={index}
selected={surveyD === selectedOption}
>
{surveyD}
</option>
))}
</Form.Select>
</FloatingLabel>
</React.Fragment>
);
};
export const SurveyRadioButton = (props) => {
const [checkedOption, setCheckedOption] = useState([]);
console.log("Checked Option", checkedOption);
function handleCheckedChange(event) {
setCheckedOption(event.target.value);
}
return (
<div className="for-spacing">
<label>{props.value.surveyQuestion}</label>
<Form>
{["radio"].map((type) => (
<div key={`inline-${type}`} className="mb-3">
{props.value.option.map((answer, index) => {
return (
<Form.Check
inline
label={answer}
key={index}
name="group1"
type="radio"
value={answer}
id={`inline-${type}-1`}
onChange={handleCheckedChange}
/>
);
})}
</div>
))}
</Form>
</div>
);
};
what shall I try, please help!
Thanks!
I have so many remarks, but let's try to focus:
If you want to have controlled inputs you need to define the whole state in the upper component (=SurveyCard) and then pass down a callback for setting state accordingly.
If you want to have uncontrolled inputs first of all you need to have one single Form, not many. So remove the Form from Dropdown and Button components and wrap your SurveyCard Card.Body with it. On submit, you need to define a strategy on how to get those values out. You can for example use FormData object and iterate over the fields or - my preferable option - use some library for uncontrolled forms (i.e. "react-hook-form).
Please see into this sandbox with a working example: https://codesandbox.io/s/summer-grass-3gn4gm.
I have tried to mock your data.
Related
Why the input only taking inputs from second input only?
import React, { useState } from "react";
import Item from "./Components/Item";
import "./ToDo.css";
function ToDo() {
let toDoIs = document.getElementById("toDoInput");
const [ToDo, setToDoIs] = useState("d");
const [ToDoArray, setToDoArray] = useState([]);
return (
<div>
<h1>ToDo</h1>
<input
id="toDoInput"
onChange={() => {
setToDoIs(toDoIs.value);
}}
type="text"
/>
<button
onClick={() => {
setToDoArray([...ToDoArray, { text: ToDo }]);
toDoIs.value = "";
}}
>
Add
</button>
<Item push={ToDoArray} />
</div>
);
}
export default ToDo;
Why the second input only works, which means whenever I use submit the value from second input only stored and displayed. I don't know why this happens.
There's a few problems here...
Don't use DOM methods in React. Use state to drive the way your component renders
Your text input should be a controlled component
When updating state based on the current value, make sure you use functional updates
import { useState } from "react";
import Item from "./Components/Item";
import "./ToDo.css";
function ToDo() {
// naming conventions for state typically use camel-case, not Pascal
const [toDo, setToDo] = useState("d");
const [toDoArray, setToDoArray] = useState([]);
const handleClick = () => {
// use functional update
setToDoArray((prev) => [...prev, { text: toDo }]);
// clear the `toDo` state via its setter
setToDo("");
};
return (
<div>
<h1>ToDo</h1>
{/* this is a controlled component */}
<input value={toDo} onChange={(e) => setToDo(e.target.value)} />
<button type="button" onClick={handleClick}>
Add
</button>
<Item push={toDoArray} />
</div>
);
}
export default ToDo;
I am new to React.js, and so far, I am loving it. I am still confused on the concept of stateful components, although. I am using Bootstrap tables to build my table, and my GET request for its data grab worked flawlessly. I am using the material-ui lib for my switch component as well (no need to reinvent the wheel here!)
Although, I am now trying to integrate a new column that will be a switch for each row in my table, and that, when toggled, changes the boolean of said switch to true/false, which will then send a PUT request down to my backend. I have not built my PUT request yet, as I cannot get this UI portion functioning. Here is my code so far, and the dumby UI works, but I don't know how to integrate the stateful render I defined in NodeTableContainer at <SwitchState/> and SwitchState(), into my definition at selectionRenderer: Switches in my NodeTable component. The stateful render does render a toggle switch under the table, essentially as its own independent component. But I want to integrate that toggle switch component in const selectRow = {mode: 'checkbox', clickToSelect: true,selectionRenderer: Switches}. Here is my code, and I hope my I have explained my issue well. I have Googled endlessly, but I believe my own ignorance has blocked my from discovering the answer I need.
Table Component (NodeTable)
import React from 'react';
import {
Row,
Col,
Card,
CardBody,
} from 'reactstrap';
import BootstrapTable from 'react-bootstrap-table-next';
import ToolkitProvider, { Search, CSVExport, ColumnToggle } from 'react-bootstrap-table2-toolkit';
import paginationFactory from 'react-bootstrap-table2-paginator';
import 'chartjs-plugin-colorschemes';
import Switches from './Switch'
const columns = OMIT
const defaultSorted = [
{
dataField: 'id',
order: 'asc',
},
]
const TableWithSearch = (props) => {
const { SearchBar } = Search;
const { ExportCSVButton } = CSVExport;
const selectRow = {
mode: 'checkbox',
clickToSelect: true,
selectionRenderer: Switches
}
return (
<Card>
<CardBody>
<h4 className="header-title">OMIT</h4>
<p className="text-muted font-14 mb-4">OMIT</p>
<ToolkitProvider
bootstrap4
keyField="fqn"
data={props.data}
columns={columns}
columnToggle
search
exportCSV={{ onlyExportFiltered: true, exportAll: false }}>
{props => (
<React.Fragment>
<Row>
<Col>
<SearchBar {...props.searchProps} />
</Col>
<Col className="text-right">
<ExportCSVButton {...props.csvProps} className="btn btn-primary">
Export CSV
</ExportCSVButton>
</Col>
</Row>
<BootstrapTable
{...props.baseProps}
bordered={false}
defaultSorted={defaultSorted}
pagination={paginationFactory({ sizePerPage: 5 })}
selectRow={selectRow}
wrapperClasses="table-responsive"
/>
</React.Fragment>
)}
</ToolkitProvider>
</CardBody>
</Card>
);
};
export default TableWithSearch;
Switch Component
// #flow
import React from 'react';
import 'chartjs-plugin-colorschemes';
import './Switch.css'
import Switch from '#material-ui/core/Switch';
export default function Switches({ isOn, handleToggle }) {
return (
<div>
<Switch
checked={isOn}
onChange={handleToggle}
name="checkedA"
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>
</div>
);
}
Parent Component (NodeTableContainer)
import axios from 'axios';
import React, { Component, useState } from 'react';
import Switch from './Switch';
import App from './index';
export default class MainComp extends React.Component {
state = {
nodesData: [],
chartRef: [],
conn: [],
switchstate: [],
}
componentDidMount() {
axios.get('OMIT')
.then(res => {
const nodestate = res.data.map(x => x.nodestate);
for (var i = 0; i < nodestate.length; i++) {
if (nodestate[i] == 'up') {
nodestate[i] = true;
}
else {
nodestate[i] = false;
}
}
this.setState({ nodesData: res.data, switchstate: nodestate });
})
}
render() {
return (
< >
<App data={this.state.nodesData} checked={this.state.switchstate} />,
<SwitchState />
</>
)
}
}
function SwitchState() {
const [value, setValue] = useState(false);
console.log(value)
return (
<div className="app">
<Switch
isOn={value}
onColor="#EF476F"
handleToggle={() => setValue(!value)}
/>
</div>
);
}
Also, my SwitchState component is in a dumby form as you will see, until I can see the log showing its boolean state changing. Also, nodestate in the NodeTableContainer was my pathetic try at pulling data via the same state data. That is nonfunctional as you will also see. I will build the state properly once I can get this figured out, or you wonderful individuals aid me in this as well. Again, I am showing my ignorance here, so if there is an easier way, or if I am using an odd flavor of libs for this, please let me know. I want to learn and thrive. If you have a solution of your own, that's a completely different flavor, I plea to you to share it! Thank you all!
I figured this out for react-bootstrap. I fat arrowed in the formatter, and passed the state to formatExtraData. I then pass state from my component that holds all state, and it works flawlessly. Time to integrate my PUT request in with the event handler!
Below are my changes in code:
Table Component
export default class TableComp extends React.Component
formatter: (cell, row, index, extra) => {
if (cell === 'up') {
cell = true
}
else {
cell = false
}
return (
<div>
<Switch
checked={cell}
onChange={extra.handle}
name={row.name}
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>
</div>
)
},
formatExtraData: { handle: this.props.handleClick }
Parent Component (Holds all state)
handleClick = (e) => {
var indexFound = this.state.data.findIndex(y => y.name === e.target.name.toString())
let data= [...this.state.data];
let item = { ...data[indexFound] }
if (item.state === 'up') {
item.state = 'down'
}
else {
item.state = 'up'
}
data[indexFound] = item
this.setState({ data})
}
I'm trying to implement in my react app, two react double listbox in my component. At the moment the listboxes are filled automatically after a get request when component mounts. I need some help on how to get the selected options in each double listbox and send them to the server as json data.
I need two arrays from these lists.
This is my dual listbox classes:
import React from 'react';
import DualListBox from 'react-dual-listbox';
import 'react-dual-listbox/lib/react-dual-listbox.css';
import 'font-awesome/css/font-awesome.min.css';
export class FirstList extends React.Component {
state = {
selected: [],
};
onChange = (selected) => {
this.setState({ selected });
};
render() {
const { selected } = this.state;
return (
<DualListBox
canFilter
filterPlaceholder={this.props.placeholder || 'Search From List 1...'}
options={this.props.options}
selected={selected}
onChange={this.onChange}
/>
);
}
}
export class SecondList extends React.Component {
state = {
selected: [],
};
onChange = (selected) => {
this.setState({ selected });
};
render() {
const { selected } = this.state;
return (
<DualListBox
canFilter
filterPlaceholder={this.props.placeholder || 'Search From List 2...'}
options={this.props.options}
selected={selected}
onChange={this.onChange}
/>
);
}
}
In my component I started importing this:
import React, { useState, useEffect } from 'react'
import LoadingSpinner from '../shared/ui-elements/LoadingSpinner';
import ErrorModal from '../shared/ui-elements/ErrorModal';
import { FirstList, SecondList } from '../shared/formElements/DualListBox';
import { useHttpClient } from '../shared/hooks/http-hook';
const MyComponent = () => {
const { isLoading, error, sendRequest, clearError } = useHttpClient();
const [loadedRecords, setLoadedRecords] = useState();
useEffect(() => {
const fetchRecords = async () => {
try {
const responseData = await sendRequest(
process.env.REACT_APP_BACKEND_URL + '/components/get'
);
setLoadedRecords(responseData)
} catch (err) { }
};
fetchRecords();
}, [sendRequest]);
...
...
return (
<React.Fragment>
<ErrorModal error={error} onClear={clearError} />
<form>
<div className="container">
<div className="row">
<div className="col-md-6">
<fieldset name="SerialField" className="border p-4">
<legend className="scheduler-border"></legend>
<div className="container">
<p>SERIALS</p>
{loadedRecords ? (
<FirstList id='Serials' options={loadedRecords.firstRecordsList} />
) : (
<div>
<label>List is loading, please wait...</label>
{isLoading && <LoadingSpinner />}
</div>
)}
</div>
</fieldset>
</div>
<div className="col-md-6">
<fieldset name="SystemsField" className="border p-4">
<legend className="scheduler-border"></legend>
<div className="container">
<p>SYSTEMS</p>
{loadedRecords ? (
<SecondList options={loadedRecords.secondRecordsList} />
) : (
<div>
<label>List is loading, please wait...</label>
{isLoading && <LoadingSpinner />}
</div>
)}
</div>
</fieldset>
</div>
...
...
If anyone could guide me it'll be much appreciated.
Thanks in advance!
FirstList and SecondList are using internal state to show the selected values. Since a parent component should do the server request, it needs access to this data. This can be achieved by a variety of options:
Let the parent component (MyComponent) handle the state completely. FirstList and SecondList would need two props: One for the currently selected values and another for the onChange event. MyComponent needs to manage that state. For example:
const MyComponent = () => {
const [firstListSelected, setFirstListSelected] = useState();
const [secondListSelected, setSecondListSelected] = useState();
...
return (
...
<FirstList options={...} selected={firstListSelected} onChange={setFirstListSelected} />
...
<SecondList options={...} selected={secondListSelected} onChange={setSecondListSelected} />
...
)
Provide only the onChange event and keep track of it. This would be very similar to the first approach, but the lists would keep managing their state internally and only notify the parent when a change happens through onChange. I usually don't use that approach since it feels like I'm managing the state of something twice and I also need to know the initial state of the two *List components to make sure I am always synchronized properly.
Use a ref, call an imperative handle when needed from the parent. I wouldn't recommend this as it's usually not done like this and it's getting harder to share the state somewhere else than inside of the then heavily coupled components.
Use an external, shared state like Redux or Unstated. With global state, the current state can be reused anywhere in the Application and it might even exist when the user clicks away / unmounts MyComponent. Additional server requests wouldn't be necessary if the user navigated away and came back to the component. Anyways, using an external global state needs additional setup and usually feels "too much" and like a very high-end solution that is probably not necessary in this specific case.
By using option 1 or 2 there is a notification for the parent component when something changed. On every change a server request could be sent (might even be debounced). Or there could be a Submit button which has a callback that sends the saved state to the server.
import axios from 'axios';
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
import StarIcon from '#material-ui/icons/Star';
import List from '#material-ui/core/List';
import ListItem from '#material-ui/core/ListItem';
import Paper from '#material-ui/core/Paper';
import Tabs from '#material-ui/core/Tabs';
import Tab from '#material-ui/core/Tab';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import TwitterIcon from '#material-ui/icons/Twitter';
import CloseIcon from '#material-ui/icons/Close';
import Highlighter from 'react-highlight-words';
class TwitterBot extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleTabState = this.handleTabState.bind(this);
}
state = {
loaded: [],
searched: [],
searchedTicker: '',
actveTab: '',
addedTickers: []
};
async componentDidMount() {
//Gathering data from heroku API I built and adding tweets to loaded array state
let feed = await axios.get('https://boiling-plains-63502.herokuapp.com/');
let tweets = feed.data;
this.setState({
loaded: tweets
});
}
handleChange = (e) => {
//Watching input and changing searchedTicker string while typing
this.setState({ searchedTicker: e.target.value });
};
handleTabChange = (event, newValue) => {
//Selecting the correct tab
this.setState({ tabValue: newValue });
};
handleTabState = (e, data) => {
//This is changing searchedTicker state to the value of whichever tab is clicked
this.setState({ searchedTicker: data });
};
showAll = () => {
//Clearing searched state
this.setState({ searchedTicker: '' });
};
addTicker = () => {
// Adding ticker to saved list
if (this.state.searchedTicker.length > 0) {
this.setState((state) => {
const tickers = state.addedTickers.push(state.searchedTicker);
return {
tickers,
searchedTicker: ''
};
});
} else {
alert('Plase enter a symbol to search');
}
};
removeTicker = (e, data) => {
// Removing tab
let tickers = this.state.addedTickers;
if (tickers.indexOf(data) === 0) {
tickers.shift();
this.showAll();
console.log('zero');
} else {
tickers.splice(tickers.indexOf(data));
this.showAll();
}
};
savedTickerFilter = (f) => {
this.setState({ searchedTicker: f.target.value });
};
render() {
//Trimming searched input to all lowercase and filtering displayed post within return based on search
let loaded = this.state.loaded,
searchedTicker = this.state.searchedTicker.trim().toLowerCase();
if (searchedTicker.length > 0) {
loaded = loaded.filter(function(i) {
return i.text.toLowerCase().match(searchedTicker);
});
}
//Copying loaded state and attempting to added individual numbers of tweets to each tab
let copyOfLoaded = [ ...this.state.loaded ];
let filterCopy = copyOfLoaded.filter(function(i) {
return i.text.toLowerCase().match(searchedTicker);
});
let numOfTweets = filterCopy.length;
return (
<div className="main" style={{ marginTop: 40 + 'px' }}>
<h4>Search a stock symbol below to find relevant tweets from Stocktwitz.</h4>
<h4>You may then press Add to Favorites to create a saved tab for later reference.</h4>
<div className="main__inner">
<TextField
type="text"
value={this.state.searchedTicker}
onChange={this.handleChange}
placeholder="Search Ticker..."
id="outlined-basic"
label="Search"
variant="outlined"
/>
<Button onClick={this.addTicker} variant="contained" color="primary">
Add to favorites <StarIcon style={{ marginLeft: 10 + 'px' }} />
</Button>
</div>
{/* This will be the Filter Tabs component and that will import the list thats below the Paper component below */}{' '}
<Paper square>
<Tabs indicatorColor="primary" textColor="primary" onChange={this.handleTabChange}>
<Tab label={<div className="tabs-label">All ({loaded.length})</div>} onClick={this.showAll} />
{//Mapping through tabs that are added in TwitterBot component and passed down as props to this component
this.state.addedTickers.map((i) => {
return (
<div className="tab-container">
<Tab
label={
<div className="tabs-label">
{i}
({numOfTweets})
</div>
}
key={i}
onClick={(e) => this.handleTabState(e, i)}
/>
<CloseIcon value={i} onClick={(e) => this.removeTicker(e, i)} />
</div>
);
})}
</Tabs>
</Paper>
<List className="tweets">
{loaded.map(function(i) {
return (
<ListItem key={i.id}>
{' '}
<TwitterIcon style={{ marginRight: 10 + 'px', color: '#1da1f2' }} />
<Highlighter
highlightClassName="YourHighlightClass"
searchWords={[ searchedTicker ]}
autoEscape={true}
textToHighlight={i.text}
/>,
</ListItem>
);
})}
</List>
</div>
);
}
}
export default TwitterBot;
Above is the entire component that holds all necessary logic.
I basically want {{numOfTweets}} within the tab-label to be static to each Tab thats mapped through once created. Right now it correctly will show how many items per tab there are while searching, and if clicked on current tab, but all tabs will react. I need them to stay static after search so if clicked on another tab, the past tab will still show how many tweets there were for that searched tab. Right now it's happening just because it's referencing the global loaded state, I just need way to copy that and render each one individually. I hope I explained that clear enough. You can see what I mean on my demo here: https://5ec5b3cfc2858ad16d22bd3c--elastic-khorana-7c2b7c.netlify.app/
I understand I need to break out and componentize this more, but I know theres has to be an easy solution, somehow using a simple functional component to wrap the Tab component or simple just the number that will be displayed. (I'm using Material UI)
Thank you, anything helps, just need to wrap my head around it.
Please check the codesandbox here https://codesandbox.io/s/proud-leftpad-j0rgd
I have added an object instead of a string for Addedtickers so that the count can be tracked and remains constant throughout. You can further optimize this , if you want to search again within each individual tab, but you get the gist.
Please let me know if this works for you
I converted a class component into a function component using hooks. Currently, I'm struggling to figure out why the checkboxes within this map is not updating with checked value, despite the onChange handler firing, and updating the array as necessary. (The onSubmit also works, and updates the value within the DB properly).
import {
Container,
Typography,
Grid,
Checkbox,
FormControlLabel,
Button
} from "#material-ui/core";
import Select from "react-select";
import localeSelect from "../services/localeSelect";
import {
linkCharactersToGame,
characterLinked,
linkCharacters
} from "../data/locales";
import dbLocale from "../services/dbLocale";
import { LanguageContext } from "../contexts/LanguageContext";
import { UserContext } from "../contexts/UserContext";
import { GameContext } from "../contexts/GameContext";
import { CharacterContext } from "../contexts/CharacterContext";
import { Redirect } from "react-router-dom";
export default function LinkCharacter() {
const { language } = useContext(LanguageContext);
const { user } = useContext(UserContext);
const { games, loading, error, success, connectCharacters } = useContext(
GameContext
);
const { characters } = useContext(CharacterContext);
const [game, setGame] = useState("");
const [selectedCharacters, setSelectedCharacters] = useState([]);
if (!user) {
return <Redirect to="/" />;
}
return (
<section className="link-character">
<Container maxWidth="sm">
<Typography variant="h5">
{localeSelect(language, linkCharactersToGame)}
</Typography>
{error && (
<p className="error">
<span>Error:</span> {error}
</p>
)}
{success && <p>{localeSelect(language, characterLinked)}</p>}
<Select
options={games.map(game => {
return {
label: dbLocale(language, game),
value: game._id
};
})}
onChange={e => {
setGame(e.value);
const selected = [];
const index = games.findIndex(x => x._id === e.value);
games[index].characters.forEach(character => {
selected.push(character._id);
});
setSelectedCharacters(selected);
}}
/>
</Container>
<Container maxWidth="md">
{game !== "" && (
<>
<Grid container spacing={2}>
{characters.map((character, index) => {
return (
<Grid item key={index} md={3} sm={4} xs={6}>
<FormControlLabel
control={
<Checkbox
value={character._id}
onChange={e => {
const index = selectedCharacters.indexOf(
e.target.value
);
if (index === -1) {
selectedCharacters.push(e.target.value);
} else {
selectedCharacters.splice(index, 1);
}
}}
color="primary"
checked={
selectedCharacters.indexOf(character._id) !== -1
}
/>
}
label={dbLocale(language, character)}
/>
</Grid>
);
})}
</Grid>
<Button
variant="contained"
color="primary"
onClick={e => {
e.preventDefault();
connectCharacters(game, selectedCharacters);
}}
>
{localeSelect(language, linkCharacters)}
</Button>
</>
)}
</Container>
</section>
);
}
I feel like there's something I'm missing within Hooks (or there's some sort of issue with Hooks handling something like this). I have been searching and asking around and no one else has been able to figure out this issue as well.
The state returned by [state, setState] = useState([]) is something that you should only be reading from. If you modify it, React won't know that the data has changed and that it needs to re-render. When you need to modify data, you have to use setState, or in your case setSelectedCharacters.
Also, modifying the data by reference might lead to unpredictable results if the array is read elsewhere, later on.
In addition to that, if you give the same value to setState, that the hook returned you in state, React will skip the update entirely. It is not a problem when using numbers or strings, but it becomes one when you use arrays, because the reference (the value React uses to tell if there is a difference) can be the same, when the content might have changed. So you must pass a new array to setState.
With that in mind, your onChange function could look like:
onChange={e => {
const index = selectedCharacters.indexOf(
e.target.value
);
if (index === -1) {
// creating a new array with [], so the original one stays intact
setSelectedCharacters([...selectedCharacters, e.target.value]);
} else {
// Array.filter also creates new array
setSelectedCharacters(selectedCharacters.filter((char, i) => i !== index));
}
}}
Doc is here https://en.reactjs.org/docs/hooks-reference.html#usestate