loading components twice, probably because of useEffect wrong set-up - reactjs

I have built a ToDo React App (https://codesandbox.io/s/distracted-easley-zjdrkv) that does the following:
User write down an item in the input bar
User hit "enter"
Item is saved into the list below (local storage, will update later)
There is some logic to parse the text and identify tags (basically if the text goes "#tom:buy milk" --> tag=tom, text=buy milk)
The problem I am facing are:
useEffect runs twice at load, and I don't understand why
After the first item gets saved, if I try saving a second item, the app crashes. Not sure why, but I feel it has to do with the point above...and maybe the event listener "onKeyDown"
App
import { useState, useEffect } from 'react'
import './assets/style.css';
import data from '../data/data.json'
import InputBar from "./components/InputBar/InputBar"
import NavBar from "./components/NavBar/NavBar"
import TabItem from "./components/Tab/TabItem"
function App() {
const [dataLoaded, setDataLoaded] = useState(
() => JSON.parse(localStorage.getItem("toDos")) || data
)
useEffect(() => {
localStorage.setItem("toDos", JSON.stringify(dataLoaded))
console.log('update')
}, [dataLoaded])
function deleteItem(id){
console.log(id)
setDataLoaded(oldData=>{
return {
...oldData,
"items":oldData.items.filter(el => el.id !== id)
}
})
}
return (
<div className='container'>
<NavBar/>
<InputBar
setNewList = {setDataLoaded}
/>
{
//Items
dataLoaded.items.map(el=>{
console.log(el)
return <TabItem item={el} key={el.id} delete={deleteItem}/>
})
}
</div>
)
}
export default App
InputBar
import { useState, useEffect } from 'react'
import { nanoid } from 'nanoid'
import '../../assets/style.css';
export default function InputBar(props){
const timeElapsed = Date.now();
const today = new Date(timeElapsed);
function processInput(s) {
let m = s.match(/^(#.+?:)?(.+)/)
if (m) {
return {
tags: m[1] ? m[1].slice(1, -1).split('#') : ['default'],
text: m[2],
created: today.toDateString(),
id:nanoid()
}
}
}
function handleKeyDown(e) {
console.log(e.target.value)
console.log(document.querySelector(".main-input-div input").value)
if(e.keyCode==13){
props.setNewList(oldData =>{
return {
...oldData,
"items" : [processInput(e.target.value), ...oldData.items]
}
}
)
e.target.value=""
}
}
return(
<div className="main-input-div">
<input type="text" onKeyDown={(e) => handleKeyDown(e)}/>
</div>
)
}
Tab
import { useState } from 'react'
import "./tab-item.css"
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faTrash } from "#fortawesome/free-solid-svg-icons";
export default function TabItem(props) {
return (
<div className="tab-item">
<div className="tab-item-text">{props.item.text}</div>
<div className="tab-item-actions">
<FontAwesomeIcon icon={faTrash} onClick={()=>props.delete(props.item.id)}/>
</div>
<div className="tab-item-details">
<div className="tab-item-details-tags">
{
props.item.tags.map(el=><div className="tab-item-details-tags-tag">{el}</div>)
}
</div>
</div>
<div className="tab-item-date">{props.item.created}</div>
</div>
)
}

The above answer is almoost correct. I am adding more info to the same concepts.
useEffect running twice:
This is most common ask in recent times. It's because the effect runs twice only in development mode & this behavior is introduced in React 18.0 & above.
The objective is to let the developer see & warn of any bugs that may appear due to a lack of cleanup code when a component unmounts. React is basically trying to show you the complete component mounting-unmounting cycle. Note that this behavior is not applicable in the production environment.
Please check https://beta-reactjs-org-git-effects-fbopensource.vercel.app/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed for a detailed explanation.
App crashes on second time: It's probably because you are trying to update the input value from event.target.value if you want to have control over the input value, your input should be a controlled component meaning, your react code should handle the onChange of input and store it in a state and pass that state as value to the input element & in your onKeyDown handler, reset the value state. That should fix the crash.
export default function InputBar(props){
const [inputVal, setInputVal] = useState("");
function handleKeyDown(e) {
console.log(e.target.value)
console.log(document.querySelector(".main-input-div input").value)
if(e.keyCode==13){
props.setNewList(oldData =>{
return {
...oldData,
"items" : [processInput(e.target.value), ...oldData.items]
}
}
)
setInputVal("")
}
}
return(
<div className="main-input-div">
<input
type="text"
value={inputVal}
onChange={(e) => {setInputVal(e.target.value)}}
onKeyDown={(e) => handleKeyDown(e)}
/>
</div>
)
}
Hope this helps. Cheers!

Your app is using strict mode, which in a development mode renders components twice to help detect bugs (https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects).
root.render(
<StrictMode>
<App />
</StrictMode>
);
As for the crash, I think it's happening due to props.setNewList being an asynchronous call and the resetting of e.target.value - something like this seemed to fix it for me:
function handleKeyDown(e) {
console.log(e.target.value)
console.log(document.querySelector(".main-input-div input").value)
if(e.keyCode==13){
const inputVal = e.target.value;
props.setNewList(oldData =>{
return {
...oldData,
"items" : [processInput(inputVal), ...oldData.items]
}
}
)
e.target.value=""
}
}
I will add, that using document.querySelector to get values isn't typical usage of react, and you might want to look into linking the input's value to a react useState hook.
https://reactjs.org/docs/forms.html#controlled-components

Related

How can I render Childs in react with useState?

I have a basic React code that create an empty list and fill it with data from the db with an useEffect. My problem is that when I try to make a map with the elements and render a new component, if I use an useState I can't do it.
Here is my code:
import "./../../../../assets/styles/logged/pedidos/pedidos.min.css"
import { Context } from "../../../../App";
import { useContext, useEffect, useState } from "react";
import { PedidoCamionInterface } from "../../../../domain/entities/pedido_camion/pedido_camion_interface";
export function Pedidos() {
const providers = useContext(Context);
const [pedidos, setPedidos] = useState<PedidoCamionInterface[]>([]);
useEffect(() => {
//TODO
providers.providers.pedidosDb.getPedidosCamion([1, 2, 3], "").then((pedidos) => {
setPedidos(pedidos);
});
}, []);
return (
<div id="content">
<h1>Pedidos de camión</h1>
<div id="pedidos">
{pedidos.map((pedido: PedidoCamionInterface) => { return pedidoDiv(pedido); })}
</div>
</div>
);
}
const pedidoDiv = (pedido: PedidoCamionInterface) => {
const [selected, setSelected] = useState<boolean>(false);
return (
<div className={`pedido ${selected ? "selected" : ""}`} key={pedido.idPedido}>
<span>{pedido.nombrePedido}</span>
<span>{pedido.tienda}</span>
</div>
);
}
This is the error that I get:
Warning: React has detected a change in the order of Hooks called by Pedidos. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Previous render Next render
------------------------------------------------------
1. useContext useContext
2. useState useState
3. useEffect useEffect
4. undefined useState
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pedidos#http://localhost:5173/src/ui/pages/logged/pedidos/pedidos.tsx?t=1667424544020:22:31
RenderedRoute#http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=396bef63:2437:7
Routes#http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=396bef63:2746:7
Router#http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=396bef63:2697:7
BrowserRouter#http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=396bef63:3079:7
App#http://localhost:5173/src/App.tsx?t=1667424544020:30:35 react-dom.development.js:86:29
Thanks for all.
Be able to render the child
I think the problem might lie in the .map() function.
Instead of
{pedidos.map((pedido: PedidoCamionInterface) => { return pedidoDiv(pedido); })}
try
{pedidos.map((pedido: PedidoCamionInterface) => (<PedidoDiv pedido={pedido} />))}
so React knows to render the component.
Also, change
const pedidoDiv = (pedido: PedidoCamionInterface) => {
to
const pedidoDiv = ({ pedido }: PedidoCamionInterface) => {
so the property pedido is destructured.

Print one phrase under another one using speech recognition

The recognition should start when I click recHandler button, then the recognized phrase should be printed below. If I click the button again the recognition should be activated and recognized phrase should be printed under the old one, as result I have to have a list of recognized phrases.
The problem is if I run this part of code setMessage([...message, transcript]) inside useEffect() I get infinite loop of re-renders.
If I run it without useEffect(), I get an error: Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
import React, { useState, useEffect } from 'react'
import React from 'react';
import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition';
const Dictaphone = () => {
const {
transcript,
listening,
resetTranscript,
browserSupportsSpeechRecognition
} = useSpeechRecognition();
const [message, setMessage] = useState([])
const item = (
<ul>
{message?.map((n) => (
<li>{n}</li>
))}
</ul>
)
if (!browserSupportsSpeechRecognition) {
return <span>Browser doesn't support speech recognition.</span>;
}
function recHandler(){
SpeechRecognition.startListening()
}
useEffect(()=>{
setMessage([...message, transcript])
})
return (
<div>
<p>Microphone: {listening ? 'on' : 'off'}</p>
<button onClick={recHandler}>Start</button>
<div>{item}</div>
</div>
);
};
Your useEffect cause re-render, you are missing []
useEffect(()=>{
setMessage([...message, transcript])
},[transcript])

Load Mantine MultiSelect data when dropdown first opened

I'd like to use Mantine's MultiSelect but only fetch from the server the dropdown data the first time users open the dropdown.
Most obvious candidate seems to be the onDropdownOpen where I could fetch the data but I don't know how to reference the element and its data in there. Thought I could reference it, but the code at the bottom on the doc as copied below doesn't compile (doesn't like the brackets after HTMLInputElement):
import { useRef } from 'react';
import { MultiSelect } from '#mantine/core';
function Demo() {
const ref = useRef<HTMLInputElement>();
return <MultiSelect ref={ref} data={[]} />;
}
Being very new to React, I feel I'm missing the obvious here so it'd be great if you could point me in the right direction.
I finally came up with this which seems to work. Would love to have some feedback as I stated above, I'm a begginer with React and coming from simple javascript it's a bit hard adapt to the philosophy.
import React, { useState, useEffect } from 'react';
import { MultiSelect, Loader } from '#mantine/core';
export default function MyDropDown() {
const [data, setData] = useState(null);
const [opened, setOpened] = useState(false);
useEffect(() => {
if (opened) {
fetch("https://www.someapi.com")
.then(response => response.json())
.then(json => {
setData(json);
});
}
}, [opened]);
return (
<MultiSelect
data={data || []}
nothingFound={data && "Nothing here"}
onDropdownOpen={() => setOpened(true)}
rightSection={!data && opened && <Loader size="xs" />}
/>
);
}

input value not updating when mutating state

While creating a little project for learning purposes I have come across an issue with the updating of the input value. This is the component (I have tried to reduce it to a minimum).
function TipSelector({selections, onTipChanged}: {selections: TipSelectorItem[], onTipChanged?:(tipPercent:number)=>void}) {
const [controls, setControls] = useState<any>([]);
const [tip, setTip] = useState<string>("0");
function customTipChanged(percent: string) {
setTip(percent);
}
//Build controls
function buildControls()
{
let controlList: any[] = [];
controlList.push(<input className={styles.input} value={tip.toString()} onChange={(event)=> {customTipChanged(event.target.value)}}></input>);
setControls(controlList);
}
useEffect(()=>{
console.log("TipSelector: useEffect");
buildControls();
return ()=> {
console.log("unmounts");
}
},[])
console.log("TipSelector: Render -> "+tip);
return (
<div className={styles.tipSelector}>
<span className={globalStyles.label}>Select Tip %</span>
<div className={styles.btnContainer}>
{
controls
}
</div>
</div>
);
}
If I move the creation of the input directly into the return() statement the value is updated properly.
I'd move your inputs out of that component, and let them manage their own state out of the TipSelector.
See:
https://codesandbox.io/s/naughty-http-d38w9
e.g.:
import { useState, useEffect } from "react";
import CustomInput from "./Input";
function TipSelector({ selections, onTipChanged }) {
const [controls, setControls] = useState([]);
//Build controls
function buildControls() {
let controlList = [];
controlList.push(<CustomInput />);
controlList.push(<CustomInput />);
setControls(controlList);
}
useEffect(() => {
buildControls();
return () => {
console.log("unmounts");
};
}, []);
return (
<div>
<span>Select Tip %</span>
<div>{controls}</div>
</div>
);
}
export default TipSelector;
import { useState, useEffect } from "react";
function CustomInput() {
const [tip, setTip] = useState("0");
function customTipChanged(percent) {
setTip(percent);
}
return (
<input
value={tip.toString()}
onChange={(event) => {
customTipChanged(event.target.value);
}}
></input>
);
}
export default CustomInput;
You are only calling buildControls once, where the <input ... gets its value only that single time.
Whenever React re-renders your component (because e.g. some state changes), your {controls} will tell React to render that original <input ... with the old value.
I'm not sure why you are storing your controls in a state variable? There's no need for that, and as you noticed, it complicates things a lot. You would basically require a renderControls() function too that you would replace {controls} with.

React: useState filter array not updating state

Edit:
My error occured because I passed an array as a second parameter to useEffect. Even though the values inside the array stayed the same, the reference changed constantly, therefore useEffect was called constantly and reset my checkbox values. That array was created by an useState call. I replaced useState by useReducer (reducer only changes the object reference if the object is actually changed) and updated some missing dependencies higher up the component tree.
Original question:
I have trouble updating a state in a functional component.
My question is somewhat similiar to this one:
React SetState doesn't call render
I'm already copying my state object (by using array.filter) instead of referencing it; but my state still doesn't update.
In order to track down the problem, I tried re-creating the problem in a minimal example:
jsfiddle
But in my minimal example, everything works as expected. I'm unable to reproduce the error.
Here is my example where the state doesn't update:
configCheckboxGroup.tsx:
import classNames from "classnames";
import React, { useState, useEffect } from "react";
import { Component } from "../../model";
import CheckboxPanel from "./panels/checkboxPanel";
interface configCheckboxGroupProps {
className?: string;
choices: Array<Component>;
selected: Array<string>;
addToCart: (items: Array<Component>) => void;
}
const ConfigCheckboxGroup: React.SFC<configCheckboxGroupProps> = ({
className,
choices,
selected,
addToCart,
}) => {
const [ selectedComp, setSelectedComp ] = useState<Array<string>>(selected);
// device loads later, selected has to be updated
useEffect(() => {
setSelectedComp(selected);
}, [selected]);
const handleOnChange = (ev: React.FormEvent, id: string) => {
console.debug(id);
console.debug(selectedComp.filter(el => el !== id));
if (selectedComp.includes(id)) {
// was already checked || this line is not working!
setSelectedComp(selectedComp.filter(el => el !== id));
} else {
// was not checked
setSelectedComp([...(selectedComp), id]);
}
const selected = choices.filter(el => selectedComp.includes(el.reference._id));
addToCart(selected);
};
return (
<div className={classNames("panellist", className)}>
{
choices.map(el => {
return (
<CheckboxPanel
image={ el.reference.picture ? el.reference.picture : undefined }
name={ el.reference.name }
id={ el.reference._id }
price={ el.reference.price ? el.reference.price :
el.price ? el.price : 0 }
key={ el._id }
checked={ selectedComp.includes(el.reference._id) }
onChange={ handleOnChange }
/>
)
})
}
<span>
{ selectedComp }
</span>
</div>
)
}
export default ConfigCheckboxGroup;
And checkboxPanel.tsx:
import classNames from "classnames";
import React from "react";
import "./checkboxPanel.scss";
import noImage from "../../../resources/images/errors/no-image.svg";
interface PanelProps {
className?: string;
image?: string;
name: string;
id: string;
price: number;
checked: boolean;
onChange: (ev: React.FormEvent, id: string) => void;
}
const CheckboxPanel: React.SFC<PanelProps> = ({
className,
image,
name,
id,
price,
checked,
onChange,
}) => {
const getImage = () => {
if (image) {
return image;
} else {
return noImage;
}
}
return (
<div className={classNames("panel", "checkbox-panel", className)}>
<div className="top">
<div className="image">
<img alt="Product" src={getImage()} />
</div>
<div className="name">
{name}
</div>
</div>
<div className="bottom">
<div className="category">
{ Number(price).toFixed(2) } €
</div>
<div>
<input type="checkbox"
checked={ checked }
onChange={ (e) => onChange(e, id) }
/>
</div>
</div>
</div>
)
};
export default CheckboxPanel;
The only difference between the examples is that in the second one, I call the handle function inside a child component. But I do the same thing on other occasions as well: I have a very similar Component configRadioGroup with radio buttons instead of checkboxes where everything works fine.
I tried playing around by manually filtering the array and trying a lot of other things, but nothing seemed to help. This is why, as a last try, I ask here (although I know that this question is not a good one due to it being very specific).
Changing the prop selected will reset selectedComp if you put a console log in your useEffect you may find that that is resetting it every time.
You need to track down where selected comes from (redux?) and how it's set (addToCart?).
A dirty fix could be to only set selectedComp when component mounts, this is dirty and will/should cause react-hooks/exhaustive-deps lint to trigger:
useEffect(() => {
setSelectedComp(selected);
}, []);
But better to track down what's going wrong with selected, if it comes from redux then maybe just use selected instead and forget about selectedComp since that is just a copy.

Resources