Set title onSubmit based on current SELECT OPTION VALUE - react - reactjs

I found this tutorial on how to create a react quizz app on youtube link to tutorial
I am trying to set the title based on the current Select Option Value when submitting the form.
Currently I managed to change the title only when a different option is selected.
import React, { useState, useEffect, useRef } from "react";
import "./App.css";
import axios from "axios";
import FlashcardList from "./components/FlashcardList";
function App() {
const [flashcards, setFlashcards] = useState([]);
const [categories, setCategories] = useState([]);
const [title, setTitle] = useState("General Knowledge");
const categoryEl = useRef();
const amountEl = useRef();
useEffect(() => {
axios.get("https://opentdb.com/api_category.php").then((res) => {
setCategories(res.data.trivia_categories);
});
}, []);
function decodeString(str) {
const textArea = document.createElement("textarea");
textArea.innerHTML = str;
return textArea.value;
}
function handleSubmit(e) {
e.preventDefault();
axios
.get("https://opentdb.com/api.php", {
params: {
amount: amountEl.current.value,
category: categoryEl.current.value,
},
})
.then((res) => {
setFlashcards(
res.data.results.map((questionItem, index) => {
const answer = decodeString(questionItem.correct_answer);
const options = [...questionItem.incorrect_answers, answer];
return {
id: `${index} - ${Date.now()}`,
question: decodeString(questionItem.question),
answer: answer,
options: options.sort(() => Math.random() - 0.5),
};
})
);
});
}
function getTitle(e) {
setTitle(e.target.options[e.target.selectedIndex].text);
}
return (
<>
<form className="header" onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="category">Category</label>
<select id="category" ref={categoryEl} onChange={getTitle}>
{categories.map((category) => {
return (
<option value={category.id} key={category.id}>
{category.name}
</option>
);
})}
</select>
</div>
<div className="form-group">
<label htmlFor="amount">Number Of Questions</label>
<input
type="number"
id="amount"
min="1"
step="1"
defaultValue={10}
ref={amountEl}
/>
</div>
<div className="form-group">
<button className="btn">Generate</button>
</div>
</form>
<div className="container">
<h1 className="title">{title}</h1>
<FlashcardList flashcards={flashcards} />
</div>
</>
);
}
export default App;
Code
Live demo

You can set the category as soon the categories are fetched. Can just use the zeroth element to set the title.
useEffect(() => { axios.get("https://opentdb.com/api_category.php").then((res) => {
setCategories(res.data.trivia_categories);
setTitle(res.data.trivia_categories[0]);
});
}, []);

Related

How to handle multiple select options submittion in react js?

I want to submit a form into mongoDB using nodejs API & reactJs. With the exception of the multiple select option, everything is operating as it should be.
Being new to react, I have no idea how to handle the multi select option's onChange method.
Here is what I've tried:
import React, { useState, useRef } from "react";
import { useForm } from "react-hook-form";
import { v4 as uuidv4 } from 'uuid';
import axios from "axios";
import Select from 'react-select';
export default function EventForm(props) {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm();
const form = useRef();
const [loading, setLoading] = useState(false);
const [info, setInfo] = useState("");
const [analysis, setAnalysis] = useState("Undefined");
const [relatedEvent, setRelatedEvent] = useState([]);
const handleInfoChange = (e) => {
setInfo(e.target.value)
}
const handleAnalysisChange = (e) => {
setAnalysis(e.target.value)
}
const handleRelatedEvents = (e) => {
setRelatedEvent(e.target.value)
}
const relatedEventsData = props.data.map(opt => ({ label: opt.info, value: opt._id }));
const onSubmit = async () => {
setLoading(true);
const MySwal = withReactContent(Swal);
const eventData = {
UUID: uuidv4(),
info: info,
analysis: analysis,
relatedEvent: relatedEvent,
}
axios
.post(`${process.env.REACT_APP_PROXY}/api/events`, eventData)
.then((res) => {
console.log(res);
setLoading(false);
MySwal.fire(
"Success!",
"A new event has been saved successfully",
"success"
);
})
.catch((error) => {
console.log(error);
});
};
return (
<div className="panel-body">
<Form
ref={form}
onSubmit={handleSubmit(onSubmit)}
className="form-horizontal"
>
<div className="row">
<div className="col-lg-6">
<div className="mb-3">
<Form.Label>Info</Form.Label>
<Form.Control
type="text"
placeholder="Enter info..."
{...register("info", { required: true })}
value={info}
onChange={handleInfoChange}
/>
{errors.info && (
<ul className="parsley-errors-list filled" id="parsley-id-7" aria-hidden="false">
<li className="parsley-required">This value is required.</li>
</ul>
)}
</div>
</div>
<div className="col-lg-6">
<div className="mb-3">
<Form.Label>Related events</Form.Label>
<Select
options={relatedEventsData}
value={relatedEvent}
isMulti
onChange={handleRelatedEvents}
/>
</div>
</div>
<div className="col-lg-12">
<Button variant="primary" type="submit">
{loading ? "Saving..." : "Save"}
</Button>
</div>
</div>
</Form>
</div>
);
}
Could you please guide me how to make it work!
Thank you
you can make use of Select onChange event handler which passes the selected options as an array as argument ..
from that you can map over it to get the values as required
something as below:
const handleChange = (opts) => {
const selectedValues = opts.map((opt) => opt.value);
setSelectedValues(selectedValues);
};
Please check the working sample for better clarity 😉 -

How to update the parent / list component from the child / detail component in ReactJS?

I am beginner and practicing on Library Management System in react. So I have components named BookDetails.js, BookList.js. BookDetails contains the form for entering Title and Description. So How can I pass the data entered from BookDetails to BookList and to dispaly from App.
import React, { useState } from 'react'
import BookList from './BookList'
const BookDetails = (props) => {
const [bookdetails, setbookDetails] = useState('')
const [desc, setDesc] = useState('')
const titleChangehandler = (e) => {
setbookDetails(e.target.value)
}
const descriptionChangehandler = (e) => {
setDesc(e.target.value)
}
const submitHandler = (e) => {
e.preventDefault()
return (
<div className='bookdetails'>
<form className='form_bookdetails' onSubmit={submitHandler}>
<div>
<label>Enter Title:</label>
<input type='text' value={bookdetails} onChange={titleChangehandler}></input>
</div>
<div>
<label>Enter Description:</label>
<input type='text' value={desc} onChange={descriptionChangehandler}></input>
</div>
<div>
<button type='submit'>Add Details</button>
</div>
</form>
</div>
)
}
}
export default BookDetails
BookList.js
import React from 'react'
import './BookList.css'
import BookDetails from './BookDetails'
const BookList = () => {
return (
<div className="booklist">
<header>BookList</header>
<BookDetails />
</div>
)
}
export default BookList
You need to use props. BookList state will have an update function that it will pass to the BookDetail via props. Example (CodeSandbox) with Todo with title & description.
BookDetail will invoke this method on every save which then would update the original list.
TodoList.js
export default function TodoList() {
const [todo, setTodo] = React.useState(null);
const [todoList, setTodoList] = React.useState([]);
React.useEffect(() => {
getTodos();
}, []);
function getTodos() {
console.log("===> fetch all todos!!");
fetchTodos().then((todos) => {
setTodoList(todos);
});
}
function editTodo(todo) {
console.log("===> set todo => ", todo);
setTodo(todo);
}
function handleUpdate(updatedTodo) {
// update Todo
const updatedTodos = todoList.map((el) =>
el.id === updatedTodo.id ? updatedTodo : el
);
setTodoList(updatedTodos);
setTodo(null);
}
return (
<div>
<ul>
{todoList.map((item) => (
<li key={item.id}>
{item.title}, {item.description}
<button onClick={() => editTodo(item)}>edit</button>
</li>
))}
</ul>
{todo && <TodoDetail todo={todo} updateTodo={handleUpdate} />}
</div>
);
}
TodoDetail.js
import React from "react";
export default function TodoDetail(props) {
const [todo, setTodo] = React.useState(props.todo);
console.log("todo =>", todo);
function handleChange(key, value) {
console.log("===> todo changed!");
setTodo({
...todo,
[key]: value
});
}
function handleSubmit() {
// api PUT on todo
console.log("===> todo edit submit!!");
props.updateTodo(todo);
}
return (
<div>
<form onSubmit={handleSubmit}>
<label htmlFor="title">
<input
value={todo.title}
onChange={(e) => handleChange("title", e.target.value)}
/>
<input
value={todo.description}
onChange={(e) => handleChange("description", e.target.value)}
/>
</label>
<button type="submit">submit</button>
</form>
</div>
);
}
You can store the list of books in your BookList component like
const [bookList, setBookList] = useState([])
This way your BookList component has access to the books. You can then create a function to add books to the list
function addBook(book) {
setBookList([...bookList, book])
}
Then pass the addBook() function to the BookDetails component to use it on submit.
<BookDetails addBook={addBook}
Now BookDetails can access the function as a prop
props.addBook("pass new book here")

React, Uncaught TypeError: recipeList.push is not a function

I get error "Uncaught TypeError: recipeList.push is not a function"
when i try to submit data to localStorage. If i dont push but just rewrite existing data withouth using push() then i get no errors. I think this should be some newbie mistake.
AddRecipe.js page looks like this
import { useNavigate } from "react-router-dom";
import NewRecipeForm from "../components/recipes/NewRecipeForm";
const AddRecipe = () => {
const Navigate = useNavigate();
const setLocalStorage = (recipe) => {
const recipeList = JSON.parse(localStorage.getItem("recipe")) || [];
recipeList.push(recipe);
localStorage.setItem("recipe", JSON.stringify(recipeList));
Navigate("/");
};
// const RecipeFormHandler = (recipeData) => {
// localStorage.setItem("recipe", JSON.stringify(recipeData));
// Navigate("/");
// };
return (
<section>
<NewRecipeForm onAddRecipe={setLocalStorage} />
</section>
);
};
export default AddRecipe;
NewRecipeForm.js looks like this and i am trying to save recipeData to localStorage
import classes from "./NewRecipeForm.module.css";
import Card from "../ui/Card";
import { useRef } from "react";
const NewRecipeForm = (props) => {
const titleRef = useRef();
const imgRef = useRef();
const ingredientsRef = useRef();
const descriptionRef = useRef();
function submitHandler(event) {
event.preventDefault();
const enteredTitle = titleRef.current.value;
const enteredImg = imgRef.current.value;
const enteredingredients = ingredientsRef.current.value;
const enteredDescription = descriptionRef.current.value;
const recipeData = {
id: (Math.random() * 100).toString(),
title: enteredTitle,
image: enteredImg,
ingredients: enteredingredients,
description: enteredDescription,
};
props.onAddRecipe(recipeData);
}
return (
<Card>
<form className={classes.form} onSubmit={submitHandler}>
<div className={classes.control}>
<label htmlFor="title">Recipe Name</label>
<input type="text" required id="title" ref={titleRef} />
</div>
<div className={classes.control}>
<label htmlFor="image">Recipe Image</label>
<input type="url" required id="image" ref={imgRef} />
</div>
<div className={classes.control}>
<label htmlFor="ingredients">Ingredients</label>
<textarea
type="text"
required
id="ingredients"
rows="5"
ref={ingredientsRef}
/>
</div>
<div className={classes.control}>
<label htmlFor="description">Description</label>
<textarea id="description" required rows="5" ref={descriptionRef} />
</div>
<div className={classes.actions}>
<button type="reset">Reset</button>
<button type="submit">Add Recipe</button>
</div>
</form>
</Card>
);
};
export default NewRecipeForm;
I just used strings instead of Ref's. The replication of it got me to the working SHORT code here: SandBox.
NewRecipeForm.js
import { NewRecipeForm } from "./Child";
export default function App() {
const setLocalStorage = (recipe) => {
const recipeList = JSON.parse(localStorage.getItem("recipe")) || [];
recipeList.push(recipe);
localStorage.setItem("recipe", JSON.stringify(recipeList));
console.log(recipeList);
};
return (
<div className="App">
<NewRecipeForm onAddRecipe={setLocalStorage} />
</div>
);
}
Child.js
export const NewRecipeForm = (props) => {
function submitHandler(event) {
const recipeData = {
id: (Math.random() * 100).toString(),
title: "enteredTitle",
image: "enteredImg",
ingredients: "enteredingredients",
description: "enteredDescription"
};
props.onAddRecipe(recipeData);
}
return <button onClick={() => submitHandler()}>Click to print</button>;
};

next.js and useSWR makes an error "Too many re-renders" and I'm not sure why?

I'm new to React and even more new to Next.js
I've got an input where the user search a city by name in a list of all the cities available.
I've read that useSWR could be interesting to use (before that I made the request with axios inside a useEffect).
Once I got the array of cities I do a map to return all the matches with the request (and then I use it to do an autocomplete).
But I get an error like this :
"Error: Too many re-renders. React limits the number of renders to prevent an infinite loop."
If I just fetch the data, it works but if I do the map on the array I get an infinite loop and I don't know why.
my code :
import React, { useState, useEffect } from "react";
import styles from "./searchBar.module.css";
import { useRouter } from "next/router";
import axios from "axios";
import useSWR from "swr";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import { faSearch, faAngleDown } from "#fortawesome/free-solid-svg-icons";
import installationType from "../public/installation_type_ids.json";
const SearchBar = ({
setSearchSport,
setSearchCity,
searchCity,
searchSport,
setSearchType,
searchType,
setPage,
}) => {
const router = useRouter();
// States for search bar request
const [city, setCity] = useState(searchCity);
const [cityData, setCityData] = useState([]);
const [sport, setSport] = useState(searchSport);
const [title, setTitle] = useState("");
const [type, setType] = useState(searchType);
const [autoComplete, setAutoComplete] = useState([]);
const [displayAutoComplete, setDisplayAutoComplete] = useState(false);
// handle submit button
const handleSubmit = (e) => {
e.preventDefault();
setSearchCity(city);
setSearchSport(sport);
type ? setSearchType(type) : setSearchType("ALL");
setPage(0);
if (router.pathname !== "/points-de-rencontre-sportive")
router.push("/points-de-rencontre-sportive");
};
const url = "https://bouge-api.herokuapp.com/v1.0/city/ids";
const fetcher = (...args) => fetch(...args).then((res) => res.json());
const { data: result, error } = useSWR(url, fetcher);
if (error) return <h1>Oups ! Une erreur est survenue...</h1>;
if (!result) return <h1>Chargement en cours...</h1>;
const handleTest = (e) => {
setCity(e.target.value);
e.target.value === 0
? setDisplayAutoComplete(false)
: setDisplayAutoComplete(true);
if (result && result.data) {
const dataMapped = result.data.map((city) => {
return { city: city.name, type: "city" };
});
let tab = [];
dataMapped.map((item, i) => {
item.name
if (item.name.toLowerCase().includes(city)) {
tab.push(item);
}
return setAutoComplete(tab);
});
}
};
// autocomplete rendering
const renderAutoComplete = autoComplete.map((elem, index) => {
console.log(elem);
if (index <= 9) {
return (
<div
className={styles.autocompleteDiv}
key={index}
onClick={() => {
if (elem.type === "city") {
setCity(elem.city);
}
if (elem.type === "sport") {
setSport(elem.sport);
}
setDisplayAutoComplete(false);
}}
>
<p>{elem.type === "city" ? elem.city : elem.sport}</p>
</div>
);
} else {
return null;
}
});
return (
<div className={styles.searchBar}>
<form className={styles.form} onSubmit={handleSubmit}>
<div>
<label htmlFor="city">Ville</label>
<input
type="text"
id="city"
placeholder="Où veux-tu jouer?"
value={city}
onChange={handleTest}
autoComplete="off" // disable chrome auto complete
/>
</div>
<div>
<label htmlFor="sport">Sport</label>
<input
type="text"
id="sport"
placeholder="Spécifie le sport"
value={sport}
onChange={(e) => {
setSport(e.target.value);
}}
autoComplete="off" // disable chrome auto complete
/>
</div>
<div>
<label htmlFor="title">Nom</label>
<input
type="text"
id="title"
placeholder="Recherche par nom"
value={title}
onChange={(e) => {
setTitle(e.target.value);
let tab = [];
installationType.map((item, i) => {
if (item.installation_type.includes(title)) {
tab.push(item);
}
return setAutoComplete(tab);
});
console.log(tab);
}}
autoComplete="off" // disable chrome auto complete
/>
</div>
<div>
<label htmlFor="type">Type</label>
<select
type="text"
id="type"
placeholder="Type de structure"
value={type}
>
<option value="ALL" defaultValue>
Type de structure
</option>
<option value="AS">Association</option>
<option value="PRIV">Privé</option>
<option value="PUB">Public</option>
<option value="EVENT">Activité</option>
</select>
<i>
<FontAwesomeIcon
icon={faAngleDown}
className={styles.selectIcon}
></FontAwesomeIcon>
</i>
</div>
<button>
<i>
<FontAwesomeIcon
icon={faSearch}
className="fa-lg"
></FontAwesomeIcon>
</i>
Rechercher
</button>
</form>
{displayAutoComplete ? (
<div className={styles.searchResults}>{renderAutoComplete}</div>
) : null}
</div>
);
};
export default SearchBar;
After you fetched data, you call setCityData method to update city data, this cause component re-render and run code in your SearchBar component again, so it call setCityData again and then continue re-render => infinite re-render.
I think you should put it into a useEffect:
useEffect(() => {
if (result && result.data) {
const dataMapped = result.data.map((city) => {
return { city: city.name, type: "city" };
});
setCityData(dataMapped)
}
}, [result])
so it will update city data only when result has data

How to dynamically load a component from an object in state array?

When I make a selection from the dropdown I saved the selected value to type then when I click the button I add an object to drums, I map over thee drums and based on the type I want to render the component with the same name.
Sandbox here
import React, { useState } from "react";
import uuid from "react-uuid";
import "./styles.css";
const Snare = () => {
return <div>Snare</div>;
};
const Gong = () => {
return <div>Gong</div>;
};
export default function App() {
const [drums, setDrums] = useState([]);
const [type, setType] = useState();
return (
<div className="App">
{drums.map((Drum, index) => (
<Drum.type /> // Why cant I use `.type`?
))}
<label>
Drum type to add:
<select onChange={e => setType(e.target.value)} value={type}>
<option value="">Select...</option>
<option value="Snare">Snare</option>
<option value="Gong">Gong</option>
</select>
<button
onClick={() => {
setDrums([...drums,
{
id: uuid(),
type
}
]);
}}
>
Add drum
</button>
</label>
</div>
);
}
In your case Drum.type is not a component but a string, you need to maintain a map of the string to component and then render it
const map = {
Snare: Snare,
Gong: Gong
};
export default function App() {
const [drums, setDrums] = useState([]);
const [type, setType] = useState();
return (
<div className="App">
{drums.map((Drum, index) => {
const Component = map[Drum.type];
return <Component key={index}/>;
})}
<label>
Drum type to add:
<select onChange={e => setType(e.target.value)} value={type}>
<option value="">Select...</option>
<option value="Snare">Snare</option>
<option value="Gong">Gong</option>
</select>
<button
onClick={() => {
setDrums([
...drums,
{
id: uuid(),
type
}
]);
}}
>
Add drum
</button>
</label>
</div>
);
}
Working demo
That's because the type is a string.
You could create a mapping to solve this and use React.createElement().
Something like:
const mapping = {
'Snare': Snare,
'Gong': Gong
}
{ drums.map(({ type }, index) => (
React.createElement(mapping[type], { key: index })
))
}

Resources