Error: Too many re-renders when trying to setState - javascript

This is my code:
import React, {useState, useEffect} from 'react';
import './App.css';
import {Table, Button, InputGroup, FormControl} from 'react-bootstrap';
import {PonCard} from "./components/PonCard";
function App() {
const [pons, setPons] = useState(null);
const [translations, setTranslations] = useState(null);
const [isInEditMode, setIsInEditMode] = useState(false);
const [inputValue, setInputValue] = useState('');
const [errors, setErrors] = useState([]);
const [translationsToSave, setTranslationsToSave] = useState([]);
const changeIsInEditMode = () => setIsInEditMode(!isInEditMode);
const handleEditButtonClick = (id) => console.log('Edit', id);
const handleDeleteButtonClick = (id) => console.log('Delete', id);
const handleInputChange = (e) => setInputValue(e.target.value);
const handleFetchOnButtonClick = async () => {
const resp = await fetch(`http://localhost:8080/pons/findTranslation/${inputValue}`).then(r => r.json()).catch(e => console.log(e));
if (resp.ok === true) {
setTranslations(resp.resp[0].hits);
setErrors([]);
} else {
setErrors(resp.errors ? resp.errors : ['Something went wrong. check the input']);
}
};
const handleSaveTranslations = async () => {
const resp = await fetch('http://localhost:8080/pons/', {
method: 'POST',
body: JSON.stringify({original: inputValue, translations: translationsToSave}),
mode: 'cors',
headers: {
'Content-Type': 'application/json',
}
}).then(r => r.json())
.catch(e => {
console.log(e);
return {ok: false};
});
setInputValue('');
setTranslations(null);
if (resp.errors) {
setErrors(resp.errors);
}
};
useEffect(() => {
fetch('http://localhost:8080/pons/')
.then(r => r.json())
.then(resp => {
if (resp.ok === true) {
setPons(resp.pons);
} else {
setErrors(resp.errors);
}
})
.catch(e => console.log(e));
}, []);
return (
<div className="App">
<InputGroup className="mb-3">
<FormControl
value={inputValue}
onChange={handleInputChange}
placeholder={inputValue}
/>
</InputGroup>
<div className="mb-3">
<Button onClick={handleFetchOnButtonClick} disabled={inputValue === '' || errors.length > 0}>Translate</Button>
<Button onClick={changeIsInEditMode}>
{isInEditMode ? 'Exit edit mode' : 'Enter edit mode'}
</Button>
<Button disabled={translationsToSave.length === 0} onClick={handleSaveTranslations}>Save translations</Button>
</div>
{errors.length > 0 ? errors.map(e => <div key={e}>{e}</div>) : null}
{
pons && !translations && inputValue === '' ? pons.map(pon => <PonCard key={Math.random()} {...{pon}}/>) : null
}
{
translations ?
<Table striped bordered hover>
<thead>
<tr>
<th>Original</th>
<th>Translation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{
translations.map(pon => pon.roms.map(rom => rom.arabs.map(arab => arab.translations.map(translation => {
const {source, target} = translation;
return (
<tr key={Math.random()}>
<td><span dangerouslySetInnerHTML={{__html: source}}/></td>
<td><span dangerouslySetInnerHTML={{__html: target}}/></td>
<td>
{
!translationsToSave.includes(target) ?
<Button onClick={() => {
setTranslationsToSave(prev => [...prev, target]);
}}>
Add translation
</Button>
:
<Button
onClick={() => {
setTranslationsToSave((prev) => {
const index = prev.findIndex(elem => elem === target)
return [...prev.slice(0, index), ...prev.slice(index + 1)]
});
}}>
Remove translation
</Button>
}
</td>
</tr>
)
}))))
}
</tbody>
</Table>
: (
<span>No translations</span>
)
}
</div>
);
}
export default App;
PonCard component:
import {Button, Card} from "react-bootstrap";
import React, {useState} from "react";
export const PonCard = ({pon}) => {
const [isFlipped, setIsFlipped] = useState(false);
const handleClick = setIsFlipped(!isFlipped);
return (
<Card style={{width: '18rem'}}>
<Card.Body>
<Card.Title>{pon.original}</Card.Title>
<Card.Text>
{pon.translations.map(translation => (
<div key={Math.random()} dangerouslySetInnerHTML={translation}/>
))}
</Card.Text>
<Button variant="primary" onClick={handleClick}>Show translations</Button>
</Card.Body>
</Card>
)
};
What I'm trying to do is to fetch data on mount. I found that this is the correct way to mimic componentDidMount
useEffect(() => {
fetch('http://localhost:8080/pons/')
.then(r => r.json())
.then(resp => {
if (resp.ok === true) {
setPons(resp.pons);
} else {
setErrors(resp.errors);
}
})
.catch(e => console.log(e));
}, []);
But I get
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
All the time.
106 | .then(r => r.json())
107 | .then(resp => {
108 | if (resp.ok === true) {
> 109 | setPons(resp.pons);
| ^ 110 | } else {
111 | setErrors(resp.errors);
112 | }
It points to the setPons method, which makes no sense, since it's only updated once on mount. What am I missing?

The issue is this line in PonCard:
const handleClick = setIsFlipped(!isFlipped);
Every time PonCard renders, this line will immediately toggle its flipped state, which renders it again and flips it again, and so on. You probably intended to do this instead:
const handleClick = () => setIsFlipped(!isFlipped);
The reason the error message points to setPons is just that that's the first set state that kicked it off. Prior to that, no PonCard was being rendered, and so there was no infinite loop of PonCard renders.

Related

useState defaults appear to rerun after running function on state change, defaults shouldnt run twice

I have the following issue with website where the settings state resets after running more searches. The settings component is show below in the picture, it usually works but if you uncheck a box and then run a few more searches at some point the showAllDividends setting will be set to false, the All dividends component won't be on the screen, but for some reason the checkbox itself is checked (true). This is my first time really working with checkboxes in React, and I think I'm using the onChange feature wrong. Right now I get the event.target.checked boolean, but only onChange.
If that isn't the issue then the most likely cause is the default statements being run again on another render:
const [showMainInfo, setShowMainInfo] = useState(true);
const [showYieldChange, setShowYieldChange] = useState(true);
const [showAllDividends, setShowAllDividends] = useState(true);
the thing is I don't see why the default statements would run more than once, the component isn't being destroyed there's no react router usage. I expected it to keep its current state after the page is first loaded. I think the settings defaults are being rerun, but just don't understand why they would.
I unchecked, checked, unchecked the 'All dividends' checkbox, and it was unchecked when I ran 2 more searches. After the second search the checkbox was checked but the component was gone, because showAllDividends was false
main component SearchPage.js:
import React, {useState, useEffect} from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import SearchBar from './SearchBar';
import AllDividendsDisplay from './dividend_results_display/AllDividendsDisplay';
import DividendResultsDisplay from './dividend_results_display/DividendResultsDisplay';
import SettingsView from './settings/SettingsView';
const HOST = process.env.REACT_APP_HOSTNAME
const PROTOCOL = process.env.REACT_APP_PROTOCOL
const PORT = process.env.REACT_APP_PORT
const BASE_URL = PROTOCOL + '://' + HOST + ':' + PORT
const SearchPage = ({userId}) => {
const DEFAULT_STOCK = 'ibm';
const [term, setTerm] = useState(DEFAULT_STOCK);
const [debouncedTerm, setDebouncedTerm] = useState(DEFAULT_STOCK);
const [loading, setLoading] = useState(false);
const [recentSearches, setRecentSearches] = useState([DEFAULT_STOCK]);
const [dividendsYearsBack, setDividendsYearsBack] = useState('3');
const [debouncedDividendYearsBack, setDebouncedDividendYearsBack] = useState('3');
const [errorMessage, setErrorMessage] = useState('');
const [dividendsData, setDividendsData] = useState(
{
current_price: '',
recent_dividend_rate: '',
current_yield: '',
dividend_change_1_year: '',
dividend_change_3_year: '',
dividend_change_5_year: '',
dividend_change_10_year: '',
all_dividends: [],
name: '',
description: '',
}
)
const [settingsViewVisible, setSettingsViewVisible] = useState(false);
const [showMainInfo, setShowMainInfo] = useState(true);
const [showYieldChange, setShowYieldChange] = useState(true);
const [showAllDividends, setShowAllDividends] = useState(true);
const onTermUpdate = (term) => {
setTerm(term)
}
// TODO: write a custom hook that debounces taking the term and the set debounced term callback
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedTerm(term);
}, 1500);
return () => {
clearTimeout(timerId);
};
}, [term]);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedDividendYearsBack(dividendsYearsBack);
}, 1500);
return () => {
clearTimeout(timerId);
};
}, [dividendsYearsBack]);
useEffect(() => {runSearch()}, [debouncedTerm]);
useEffect(() => {
// alert(dividendsYearsBack)
if (dividendsYearsBack !== '') {
runSearch();
}
}, [debouncedDividendYearsBack])
useEffect(() => {
console.log("user id changed")
if (userId) {
const user_profile_api_url = BASE_URL + '/users/' + userId
axios.get(user_profile_api_url, {})
.then(response => {
const recent_searches_response = response.data.searches;
const new_recent_searches = [];
recent_searches_response.map(dict => {
new_recent_searches.push(dict.search_term)
})
setRecentSearches(new_recent_searches);
})
.catch((error) => {
console.log("error in getting user profile: ", error.message)
})
}
}, [userId])
useEffect(() => {
const user_profile_api_url = BASE_URL + '/users/' + userId
const request_data = {searches: recentSearches}
axios.post(user_profile_api_url, request_data)
// .then(response => {
// console.log(response)
// })
}, [recentSearches])
const makeSearchApiRequest = () => {
let dividends_api_url = BASE_URL + '/dividends/' + term + '/' + dividendsYearsBack
if (!recentSearches.includes(term)) {
setRecentSearches([...recentSearches, term])
}
axios.get(dividends_api_url, {})
.then(response => {
// console.log(response)
setLoading(false);
setDividendsData(response.data);
})
.catch((error) => {
console.log(error.message);
setLoading(false);
setErrorMessage(error.message);
})
}
const runSearch = () => {
console.log("running search: ", term);
setErrorMessage('');
if (term) {
setLoading(true);
if (!dividendsYearsBack) {
setDividendsYearsBack('3', () => {
makeSearchApiRequest()
});
} else {
makeSearchApiRequest()
}
}
}
const recentSearchOnClick = (term) => {
setTerm(term);
setDebouncedTerm(term);
}
const removeRecentSearchOnClick = (term) => {
const searchesWithoutThisOne = recentSearches.filter(search => search !== term)
setRecentSearches(searchesWithoutThisOne);
}
const dividendsYearsBackOnChange = (text) => {
setDividendsYearsBack(text);
}
const renderMainContent = () => {
if (!debouncedTerm) {
return (
<div className="ui active">
<div className="ui text">Search for info about a stock</div>
</div>
)
}
if (loading === true) {
return (
<div className="ui active dimmer">
<div className="ui big text loader">Loading</div>
</div>
)
}
if (errorMessage) {
return (
<div className="ui active">
<div className="ui text">{errorMessage}</div>
</div>
)
} else {
return (
<DividendResultsDisplay
data={dividendsData}
dividends_years_back={dividendsYearsBack}
dividendsYearsBackOnChange={dividendsYearsBackOnChange}
showMainInfo={showMainInfo}
showYieldChange={showYieldChange}
showAllDividends={showAllDividends}/>
)
}
}
// https://stackoverflow.com/questions/38619981/how-can-i-prevent-event-bubbling-in-nested-react-components-on-click
const renderRecentSearches = () => {
return recentSearches.map((term) => {
return (
<div key={term}>
<button
onClick={() => recentSearchOnClick(term)}
style={{marginRight: '10px'}}
>
<div>{term} </div>
</button>
<button
onClick={(event) => {event.stopPropagation(); removeRecentSearchOnClick(term)}}>
X
</button>
<br/><br/>
</div>
)
})
}
const renderSettingsView = (data) => {
if (settingsViewVisible) {
return (
<SettingsView data={data} />
)
} else {
return null;
}
}
const toggleSettingsView = () => {
setSettingsViewVisible(!settingsViewVisible);
}
const toggleDisplay = (e, setter) => {
setter(e.target.checked)
}
const SETTINGS_DATA = [
{
label: 'Main info',
id: 'main_info',
toggler: toggleDisplay,
setter: setShowMainInfo
},
{
label: 'Yield change',
id: 'yield_change',
toggler: toggleDisplay,
setter: setShowYieldChange
},
{
label: 'Dividends list',
id: 'all_dividends',
toggler: toggleDisplay,
setter: setShowAllDividends
},
]
console.log("showMainInfo: ", showMainInfo);
console.log("showYieldChange: ", showYieldChange);
console.log("showAllDividends: ", showAllDividends);
return (
<div className="ui container" style={{marginTop: '10px'}}>
<SearchBar term={term} onTermUpdate={onTermUpdate} />
{renderRecentSearches()}
<br/><br/>
<button onClick={toggleSettingsView}>Display settings</button>
{renderSettingsView(SETTINGS_DATA)}
<div className="ui segment">
{renderMainContent()}
</div>
</div>
)
}
const mapStateToProps = state => {
return { userId: state.auth.userId };
};
export default connect(
mapStateToProps
)(SearchPage);
// export default SearchPage;
the settingsView component:
import React from 'react';
import SettingsCheckbox from './SettingsCheckbox';
const SettingsView = ({data}) => {
const checkboxes = data.map((checkbox_info) => {
return (
<div key={checkbox_info.id}>
<SettingsCheckbox
id={checkbox_info.id}
label={checkbox_info.label}
toggler={checkbox_info.toggler}
setter={checkbox_info.setter}/>
<br/>
</div>
)
});
return (
<div className="ui segment">
{checkboxes}
</div>
);
}
export default SettingsView;
SettingsCheckbox.js:
import React, {useState} from 'react';
const SettingsCheckbox = ({id, label, toggler, setter}) => {
const [checked, setChecked] = useState(true)
return (
<div style={{width: '200px'}}>
<input
type="checkbox"
checked={checked}
id={id}
name={id}
value={id}
onChange={(e) => {
setChecked(!checked);
toggler(e, setter);
}} />
<label htmlFor="main_info">{label}</label><br/>
</div>
);
}
export default SettingsCheckbox;

How to avoid prop drilling ? / How to use useContext?

I'm working on a React Notes Application and my App.js contains all the necessary functions props which are passed down to several components.
As a result I'm doing prop drilling a lot where I'm passing down around 10-20 props/functions in the components where it isn't needed.
I tried using useContext Hook but I guess it doesn't work with callback functions in the value parameter.
App.js
const App = () => {
const [ notes, setNotes ] = useState([]);
const [ category, setCategory ] = useState(['Notes']);
const [ searchText, setSearchText ] = useState('');
const [ alert, setAlert ] = useState({
show:false,
msg:'',
type:''
});
const [isEditing, setIsEditing] = useState(false);
const [editId, setEditId] = useState(null);
useEffect(()=>{
keepTheme();
})
// retreive saved notes
useEffect(()=>{
const savedNotes = JSON.parse(localStorage.getItem('react-notes-data'));
if (savedNotes) {
setNotes(savedNotes)
}
}, []);
// save notes to local storage
useEffect(() => {
localStorage.setItem('react-notes-data', JSON.stringify(notes))
setNotesCopy([...notes]);
}, [notes]);
// save button will add new note
const addNote = (text) => {
const date = new Date();
const newNote = {
id: nanoid(),
text: text,
date: date.toLocaleDateString(),
category: category,
}
const newNotes = [...notes, newNote]
setNotes(newNotes);
}
const deleteNote = (id) => {
showAlert(true, 'Note deleted', 'warning');
const newNotes = notes.filter(note => note.id !== id);
setNotes(newNotes);
}
// hardcoded values for categories
const allCategories = ['Notes', 'Misc', 'Todo', 'Lecture Notes', 'Recipe'];
// copy notes for filtering through
const [notesCopy, setNotesCopy] = useState([...notes]);
const handleSidebar = (category) => {
setNotesCopy(category==='Notes'?[...notes]:
notes.filter(note=>note.category===category));
}
// function to call alert
const showAlert = (show=false, msg='', type='') => {
setAlert({show, msg, type});
}
return (
<div>
<div className="container">
<Sidebar
allCategories={allCategories}
handleSidebar={handleSidebar}
notesCopy={notesCopy}
key={notes.id}
/>
<Header notes={notes} alert={alert} removeAlert={showAlert} />
<Search handleSearchNote={setSearchText} />
<NotesList
notesCopy={notesCopy.filter(note=>
note.text.toLowerCase().includes(searchText) ||
note.category.toString().toLowerCase().includes(searchText)
)}
handleAddNote={addNote}
deleteNote={deleteNote}
category={category}
setCategory={setCategory}
allCategories={allCategories}
showAlert={showAlert}
notes={notes}
setNotes={setNotes}
editId={editId}
setEditId={setEditId}
isEditing={isEditing}
setIsEditing={setIsEditing}
/>
</div>
</div>
)
}
NotesList.js
const NotesList = (
{ notesCopy, handleAddNote, deleteNote, category, setCategory, showHideClassName, allCategories, showAlert, isEditing, setIsEditing, notes, setNotes, editId, setEditId }
) => {
const [ noteText, setNoteText ] = useState('');
const textareaRef = useRef();
// function to set edit notes
const editItem = (id) => {
const specificItem = notes.find(note=>note.id === id);
setNoteText(specificItem.text);
setIsEditing(true);
setEditId(id);
textareaRef.current.focus();
}
return (
<div key={allCategories} className="notes-list">
{notesCopy.map(note => {
return (
<Note
key={note.id}
{...note}
deleteNote={deleteNote}
category={note.category}
isEditing={isEditing}
editId={editId}
editItem={editItem}
/>)
})}
<AddNote
handleAddNote={handleAddNote}
category={category}
setCategory={setCategory}
showHideClassName={showHideClassName}
allCategories={allCategories}
showAlert={showAlert}
isEditing={isEditing}
setIsEditing={setIsEditing}
notes={notes}
setNotes={setNotes}
editId={editId}
setEditId={setEditId}
noteText={noteText}
setNoteText={setNoteText}
textareaRef={textareaRef}
/>
</div>
)
}
AddNote.js
const AddNote = ({ notes, setNotes, handleAddNote, category, setCategory, showHideClassName, allCategories, showAlert, isEditing, setIsEditing, editId, setEditId, noteText, setNoteText, textareaRef }) => {
const [ show, setShow ] = useState(false);
const [ modalText, setModalText ] = useState('');
const charCount = 200;
const handleChange = (event) => {
if (charCount - event.target.value.length >= 0) {
setNoteText(event.target.value);
}
}
const handleSaveClick = () => {
if (noteText.trim().length === 0) {
setModalText('Text cannot be blank!');
setShow(true);
}
if (category === '') {
setModalText('Please select a label');
setShow(true);
}
if (noteText.trim().length > 0 && category!=='') {
showAlert(true, 'Note added', 'success');
handleAddNote(noteText);
setNoteText('');
setShow(false);
}
if (noteText.trim().length > 0 && category!=='' && isEditing) {
setNotes(notes.map(note=>{
if (note.id === editId) {
return ({...note, text:noteText, category:category})
}
return note
}));
setEditId(null);
setIsEditing(false);
showAlert(true, 'Note Changed', 'success');
}
}
const handleCategory = ( event ) => {
let { value } = event.target;
setCategory(value);
}
showHideClassName = show ? "modal display-block" : "modal display-none";
return (
<div className="note new">
<textarea
cols="10"
rows="8"
className='placeholder-dark'
placeholder="Type to add a note.."
onChange={handleChange}
value={noteText}
autoFocus
ref={textareaRef}
>
</textarea>
<div className="note-footer">
<small
className='remaining'
style={{color:(charCount - noteText.length == 0) && '#c60000'}}>
{charCount - noteText.length} remaining</small>
<div className='select'>
<select
name={category}
className="select"
onChange={(e)=>handleCategory(e)}
required
title='Select a label for your note'
defaultValue="Notes"
>
<option value="Notes" disabled selected>Categories</option>
{allCategories.map(item => {
return <option key={item} value={item}>{item}</option>
})}
</select>
</div>
<button className='save' onClick={handleSaveClick} title='Save note'>
<h4>{isEditing ? 'Edit':'Save'}</h4>
</button>
</div>
{/* Modal */}
<main>
<div className={showHideClassName}>
<section className="modal-main">
<p className='modal-text'>{modalText}</p>
<button type="button" className='modal-close-btn'
onClick={()=>setShow(false)}><p>Close</p>
</button>
</section>
</div>
</main>
</div>
)
}
I want the functions passed from App.js to NotesList.js to be in AddNote.js without them being passed in NotesList.js basically minimizing the props destructuring in NotesList.js
Context API does work with function. What you need to do is pass your function to Provider inside value :
<MyContext.Provider value={{notes: notesData, handler: myFunction}} >
For example:
// notes-context.js
import React, { useContext, createContext } from 'react';
const Context = createContext({});
export const NotesProvider = ({children}) => {
const [notes, setNote] = useState([]);
const addNote = setNote(...); // your logic
const removeNote = setNote(...); // your logic
return (
<Context.Provider value={{notes, addNote, removeNote}}>
{children}
</Context.Provider>
)
}
export const useNotes = () => useContext(Context);
Add Provider to your App.js like so:
// App.js
import NoteProvider from './notes-context';
export default App = () => {
return (
<NoteProvider>
<div>... Your App</div>
</NoteProvider>
)
}
Then call UseNote in your NoteList.js to use the function:
// NoteList.js
import {useNotes} from './note-context.js';
export const NoteList = () => {
const {notes, addNotes, removeNotes} = useNotes();
// do your stuff. You can now use functions addNotes and removeNotes without passing them down the props
}

Get an Error that .filter is not a function

I have tried to create an autocomplete suggestion box from an Thailand's province database URL.
This is my source code. I export this to App.js in src directory
import React, { useEffect, useState, useRef } from "react";
const Test = () => {
const [display, setDisplay] = useState(false);
const [singleProvince, setSingleProvince] = useState([]);
const [singleProvinceData, setSingleProvinceData] = useState([]);
const [search, setSearch] = useState("");
const wrapperRef = useRef(null);
const province_dataBase_url = 'https://raw.githubusercontent.com/earthchie/jquery.Thailand.js/master/jquery.Thailand.js/database/raw_database/raw_database.json'
useEffect(() => {
const promises = new Array(20).fill(fetch(province_dataBase_url)
.then((res) => {
return res.json().then((data) => {
const createSingleProvince = data.filter( (each) => {
if (false == (singleProvince.includes(each.province))) {
setSingleProvince(singleProvince.push(each.province))
setSingleProvinceData(singleProvinceData.push(each))
}
})
return data;
}).catch((err) => {
console.log(err);
})
}))
}, [])
useEffect(() => {
window.addEventListener("mousedown", handleClickOutside);
return () => {
window.removeEventListener("mousedown", handleClickOutside);
};
});
const handleClickOutside = event => {
const { current: wrap } = wrapperRef;
if (wrap && !wrap.contains(event.target)) {
setDisplay(false);
}
};
const updateProvince = inputProvince => {
setSearch(inputProvince);
setDisplay(false);
};
return (
<div ref={wrapperRef} className="flex-container flex-column pos-rel">
<input
id="auto"
onClick={() => setDisplay(!display)}
placeholder="Type to search"
value={search}
onChange={event => setSearch(event.target.value)}
/>
{display && (
<div className="autoContainer">
{ singleProvinceData
.filter( ({province}) => province.indexOf(search.toLowerCase()) > -1)
.map( (each,i) => {
return (
<div
onClick={() => updateProvince(each.province)}
className="singleProvinceData"
key={i}
tabIndex="0"
>
<span>{each.province}</span>
</div>
)
})}
</div>
)}
</div>
);
}
export default Test
When click on an input box, the console says "TypeError: singleProvinceData.filter is not a function"
enter image description here
I cannot find out what's wrong with my code
The issue is with the "singleProvinceData" state is not set correctly.
you cannot push data directly into the state.
useEffect(() => {
const promises = new Array(20).fill(fetch(province_dataBase_url)
.then((res) => {
return res.json().then((data) => {
const shallowSingleProvinceList = [];
const shallowSingleProvinceDataList = [];
const createSingleProvince = data.filter( (each) => {
if (false == (singleProvince.includes(each.province))) {
shallowSingleProvinceList.push(each.province)
shallowSingleProvinceDataList.push(each)
}
})
setSingleProvince(shallowSingleProvinceList)
setSingleProvinceData(shallowSingleProvinceDataList)
return data;
}).catch((err) => {
console.log(err);
})
}))
}, [])
You can show the data conditionally
{display && (
<div className="autoContainer">
{ singleProvinceData && singleProvinceData
.filter( ({province}) => province.indexOf(search.toLowerCase()) > -1)
.map( (each,i) => {
return (
<div
onClick={() => updateProvince(each.province)}
className="singleProvinceData"
key={i}
tabIndex="0"
>
<span>{each.province}</span>
</div>
)
})}
</div>
)}

How to toggle the icon only if id matches?

After receiving data from the API then mapping it out - I tried toggling one icon, but it toggles all the icons that were mapped out. I am trying to click one of the icon and not all.
import { FaRegHeart, FaHeart } from "react-icons/fa";
import { IconContext } from 'react-icons';
import { useState, useEffect } from 'react';
const Replies = ({ comments, id }) => {
const [liked, setLiked] = useState(false);
const [replies, setReplies] = useState(null);
useEffect(() => {
const abortCont = new AbortController();
fetch('http://192.168.1.98:5000/blogs/' + id, { signal: abortCont.signal })
.then(res => {
if (!res.ok) {
throw new Error('Could not get data from the database');
}
return res.json()
})
.then(data => setReplies(data))
.catch(err => {
if (err.name === 'AbortError') {
// empty if statement
}})
return () => abortCont.abort()
}, [liked])
const likeComment = () => {liked ? setLiked(false) : setLiked(true)}
return (
<>
{replies && <div className="commentSection">
{
replies.discussion.map((discussion) => (
<div key={discussion.id}>
<div className="commentEmoji">
{
liked ? <IconContext.Provider value={{ color: 'red', className: 'love-icon' }}>
<FaHeart onClick={likeComment} />
</IconContext.Provider>
: <FaRegHeart onClick={likeComment} />}
</div>
</div>
))}
</div>
}
</>
);
}
export default Replies;
This is what the API looks like:
"discussion": [
{
"id": 1,
"liked": false,
"thumbnail": "https://randomuser.me/api/portraits/thumb/men/75.jpg",
"user": "Kayode",
"comment": "They know electric vehicles are the future 👌🏾",
"replies": ""
},
{
"user": "Farook",
"liked": true,
"id": 2,
"thumbnail": "https://randomuser.me/api/portraits/thumb/men/74.jpg",
"comment": "I hope they keep buying. TSLA to that freaking moon 🚀",
"replies": ""
}
]
They have a discussion.liked property and I've been trying to access individual and not both.
If I use:
<FaRegHeart onClick={()=> likeComment(discussion.id)} />
Then
const likeComment = (id) => {
console.log(id) //it logs the exact id of the icon i.e 2
}
Try something like this, and pass appropriate props in LikedButton component
import { FaRegHeart, FaHeart } from "react-icons/fa";
import { IconContext } from 'react-icons';
import { useState, useEffect } from 'react';
const LikedButton = () => {
const [liked, setLiked] = useState(false);
const likeComment = () => {liked ? setLiked(false) : setLiked(true)}
return (
<div >
<div className="commentEmoji">
{
liked ? <IconContext.Provider value={{ color: 'red', className: 'love-icon' }}>
<FaHeart onClick={likeComment} />
</IconContext.Provider>
: <FaRegHeart onClick={likeComment} />}
</div>
</div>
)
}
const Replies = ({ comments, id }) => {
const [replies, setReplies] = useState(null);
useEffect(() => {
const abortCont = new AbortController();
fetch('http://192.168.1.98:5000/blogs/' + id, { signal: abortCont.signal })
.then(res => {
if (!res.ok) {
throw new Error('Could not get data from the database');
}
return res.json()
})
.then(data => setReplies(data))
.catch(err => {
if (err.name === 'AbortError') {
// empty if statement
}})
return () => abortCont.abort()
}, [])
return (
<>
{replies && <div className="commentSection">
{
replies.discussion.map((discussion, index) => (
<LikedButton key={index} />
))}
</div>
}
</>
);
}
export default Replies;
import LikedComment from './LikedComment';
import { useState, useEffect } from 'react';
const Replies = ({ comments, id }) => {
const [error, setError] = useState(false);
const [replies, setReplies] = useState(null);
useEffect(() => {
const abortCont = new AbortController();
fetch('http://192.168.1.98:5000/blogs/' + id, { signal: abortCont.signal })
.then((res) => {
if (!res.ok) {
throw new Error('Could not get data from the database');
}
return res.json();
})
.then((data) => {
setReplies(data);
})
.catch((err) => {
if (err.name === 'AbortError') {
// empty if statement
} else {
setError(true);
}
});
return () => abortCont.abort();
}, [id]);
const likeComment = (id) => {
setReplies({
...replies,
discussion: replies.discussion.map((reply) => {
if (reply.id === id) {
return { ...reply, liked: !reply.liked };
} else {
return reply;
}
}),
});
};
return (
<>
{error && <div className="commentError">Cannot retrieve comments </div>}
{replies && (
<div className="commentSection">
{replies.discussion.map((discussion, index) => (
<div key={discussion.id}>
<div className="comment">
<img className="commentImg" src={discussion.thumbnail} alt="" />
<div className="commentBody">
<p>
{' '}
<span>{discussion.user}</span> {discussion.comment}
</p>
</div>
<div className="commentEmoji">
<LikedComment
liked={discussion.liked}
key={index}
likeComment={() => likeComment(discussion.id)}
/>
</div>
</div>
</div>
))}
</div>
)}
</>
);
};
export default Replies;

react not rerendering after state change

I know there have been similar questions, but I have a weird issue.
This is what I'm doing
import React, {useState} from 'react';
import './App.css';
import {Table, Button, InputGroup, FormControl} from 'react-bootstrap';
function App() {
const [pons, setPons] = useState();
const [translations, setTranslations] = useState([]);
const [isInEditMode, setIsInEditMode] = useState(false);
const [inputValue, setInputValue] = useState('samochod');
const [errors, setErrors] = useState([]);
const [translationsToSave, setTranslationsToSave] = useState([]);
const changeIsInEditMode = () => setIsInEditMode(!isInEditMode);
const handleEditButtonClick = (id) => console.log('Edit', id);
const handleDeleteButtonClick = (id) => console.log('Delete', id);
const handleInputChange = (e) => setInputValue(e.target.value);
const handleFetchOnButtonClick = async () => {
const resp = await fetch(`http://localhost:8080/pons/findTranslation/${inputValue}`).then(r => r.json()).catch(e => console.log(e));
if (resp.ok === true) {
setTranslations(resp.resp[0].hits);
setErrors([]);
} else {
setErrors(resp.errors ? resp.errors : ['Something went wrong. check the input']);
}
};
const handleSaveTranslations = async () => {
const resp = await fetch('localhost:8080/pons/', {method: 'POST', body: {content: translationsToSave}});
if (resp.ok === true) {
setInputValue('');
setTranslations(null);
}
};
return (
<div className="App">
{errors.length > 0 ? errors.map(e => <div key={e}>{e}</div>) : null}
<InputGroup className="mb-3">
<FormControl
value={inputValue}
onChange={handleInputChange}
placeholder={inputValue}
/>
</InputGroup>
<div className="mb-3">
<Button onClick={handleFetchOnButtonClick} disabled={inputValue === '' || errors.length > 0}>Translate</Button>
<Button onClick={changeIsInEditMode}>
{isInEditMode ? 'Exit edit mode' : 'Enter edit mode'}
</Button>
<Button disabled={translationsToSave.length === 0} onClick={handleSaveTranslations}>Save translations</Button>
</div>
<Table striped bordered hover>
<thead>
<tr>
<th>Original</th>
<th>Translation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{translations ? translations.map(pon => pon.roms.map(rom => rom.arabs.map(arab => arab.translations.map(translation => {
const {source, target} = translation;
return (
<tr>
<td><span dangerouslySetInnerHTML={{__html: source}}/></td>
<td><span dangerouslySetInnerHTML={{__html: target}}/></td>
<td>
{
!translationsToSave.includes(target) ?
<Button onClick={() => {
const tmp = translationsToSave;
tmp.push(target);
setTranslationsToSave(tmp);
}}>
Add translation
</Button>
:
<Button
onClick={() => {
const tmp = translationsToSave;
tmp.splice(tmp.findIndex(elem => elem === target));
setTranslationsToSave(tmp);
}}>
Remove translation
</Button>
}
</td>
</tr>
)
})))) : (
<div>No translations</div>
)}
</tbody>
</Table>
</div>
);
}
export default App;
So it's a basic app, it right now just adds and removes from an array wit setTranslationsToSave. After I click the Add translation button the view stays the same. But it refreshes when I click Enter edit mode. Same with Remove translation. I need to click Enter/Exit edit mode.
Hitting Translate also reloads the view. So the Add/Remove translation buttons are the only ones which do not refresh the page. Why? What am I missing?
The issue is that you are mutating the satte in Add/Remove translation button, so when react check before re-rendering if the state updater was called with the same state it feels that nothing has changed as it does a reference check and ehnce doesn't trigger re-render
Also while updating current state based on previous state use functional callback approach for state updater.
Update your state like below
<Button onClick={() => {
setTranslationsToSave(prev => [...prev, target]);
}}>
Add translation
</Button>
:
<Button
onClick={() => {
setTranslationsToSave((prev) => {
const index = prev.findIndex(elem => elem === target)); return [...prev.slice(0, index), ...prev.slice(index + 1)]
});
}}>
Remove translation
</Button>
In your Add translation click handler, you're mutating the state:
<Button onClick={() => {
// tmp is just a reference to state
const tmp = translationsToSave;
// You are mutating state, this will be lost
tmp.push(target);
setTranslationsToSave(tmp);
}}>
You should duplicate the state and add the new element:
<Button onClick={() => {
setTranslationsToSave([...translationsToSave, target]);
}}>

Categories

Resources