Make a new array and render it from map - javascript

So I've been stuck on this for a week now even though it's probably easy... I'm trying to make an array of objects using map over a hook. The final objective is to create a new hook from this new array and to be able to interact with it with some callbacks.
The issue is that when I try to just render it (before creating a hook from it), I have this error showing up :
Uncaught TypeError: Cannot read properties of undefined (reading 'value')
So here is my code :
import { useState } from 'react'
import { useEffect } from 'react'
import { nanoid } from 'nanoid'
import './App.css'
import FirstPage from './components/FirstPage'
import Quiz from './components/Quiz'
function App() {
const [startGame, setStartGame] = useState(false)
const [quizData, setQuizData] = useState([])
useEffect(()=>{
fetch(`https://opentdb.com/api.php?amount=5`)
.then (res => res.json())
.then (data => setQuizData(data.results))
}, [])
function gameStart(){
setStartGame(true)
}
const displayQuestions = quizData.map(({question, correct_answer, incorrect_answers})=>{
const answer=[]
const answers=[]
const questions ={
value : question,
id : nanoid()
}
const answer1={
text : incorrect_answers.slice(0,1),
is_selected : false,
is_correct : false,
id : nanoid()}
const answer2={
text : incorrect_answers.slice(1,2),
is_selected : false,
is_correct : false,
id : nanoid()}
const answer3={
text : correct_answer,
is_selected : false,
is_correct : true,
id : nanoid()}
const answer4={
text : incorrect_answers.slice(2),
is_selected : false,
is_correct : false,
id : nanoid()}
const answersOptions = answers.push(answer1,answer2,answer3,answer4)
const quiz = answer.push(questions, answers)
return <div>
{
answer.map(({questions, answers}) => (
<ul key={questions.id} >
<li className='questions'>{questions.value}</li>
<button className='answers'>{answers.text}</button>
</ul>
))
}
</div>
})
return (
<>
<h1 className='bloc'></h1>
<h1 className='bloc2'></h1>
{startGame
?
<Quiz displayQuestions={displayQuestions}/>
:
<FirstPage gameStart={gameStart}/>
}
</>
);
}
export default App
The main project is to make a quiz using some datas from an API.
Thank you for your help !

First of all, you can't just throw everything in the answer array and destruct it that way. Since you're getting the elements one by one from the quizData array anyway, you're only interested in a single question here, not questions. You must write the question on one line in the jsx you return and return the answers in the loop, otherwise you will have rewritten the same question for each answer.
const displayQuestions = quizData.map(
({ question, correct_answer, incorrect_answers }) => {
const answers = [
{
text: correct_answer,
is_selected: false,
is_correct: true,
id: nanoid()
},
...incorrect_answers.map((inc) => ({
text: inc,
is_selected: false,
is_correct: false,
id: nanoid()
}))
];
const q = {
value: question,
id: nanoid()
};
// to shuffle
let shuffledAnswers = answers
.map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value);
return (
<div>
<div key={q.id}>{q.value}</div> {/* question part */}
{shuffledAnswers.map(({ id, text, is_selected, is_correct }) => (
<ul key={id}>
<li className="answers">
{text}
</li>
</ul>
))}
</div>
);
}
);

The issue is that you are trying to push the questions and answers variables as items to the answer array, but in fact you need to create an object with the questions and answers properties and push to the array after.
Doing the way you did, the array would be equal to [questions, answers], but in fact you want it to be [{questions, answers}], then you could destruct the item and get the questions and answers properties.
So, in the line:
const quiz = answer.push(questions, answers);
It should be:
const quiz = answer.push({questions, answers});
But, the answer array is not necessary in this situation, since it's going to have just 1 item anyway.

Related

React | Adding and deleting object in React Hooks (useState)

How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};

React State Changing Without Explicitly Telling it to Change [duplicate]

This question already has answers here:
Updating an object with setState in React
(23 answers)
Why can't I directly modify a component's state, really?
(7 answers)
How to update nested state properties in React
(36 answers)
Closed 12 months ago.
My react state is changing without me calling my setState function from useState hook.
After some quick research, i've narrowed it down to the fact an array is a reference type so data and tempData share the same reference and change with each other. A solution I found was to stringify then parse the data: JSON.parse(JSON.stringify(data)) but this could have some pretty bad performance hits if it's a large object right? Is there a better solution here? Redux? Or is that unnecessary? This is a pretty common case isn't it?
For anyone who cares, this works too but is kinda ugly:
change state to object rather than array
const defaultData = {
data: [
{id:0, foo:1, bar:2},
{id:1, foo:3, bar:4},
{id:2, foo:4, bar:6},
]
}
const handleData = (id) => {
setData((prevState) => {
return {data: data.data.map((i) => i.id === id ? {...i, id:i.id+10} : {...i})}
})
}
I've attached an example below which can be easily created from create-react-app.
App.js
import Child from './Child';
import { useState } from 'react';
const defaultData = [
{id:0, foo:1, bar:2},
{id:1, foo:3, bar:4},
{id:2, foo:4, bar:6},
]
function App() {
const [data, setData] = useState(defaultData)
const handleData = (id) => {
const tempData = data
for (const idx in tempData) {
const item = tempData[idx]
if (item.id === id) {
tempData[idx].id += 10
}
}
}
return (
<div>
<Child data={data} handleData={handleData} />
</div>
);
}
export default App;
Child.js
export default function Child(props) {
const {data, handleData} = props
return (
<div>
<ul>
{data.map((i) => (
<li key={i.id}>
<button onClick={() => handleData(i.id)}>
{i.foo} {i.bar}
</button>
</li>
))}
</ul>
</div>
)
}

Javascript how to return data from object with matching key

I have an array of objects each with name, height and mass. I have a component that gets the names and displays them to the browser. What I'm trying to do is, in a separate component, get the height and mass that correlates to each name.
For example I have:
[
{name: 'Luke Skywalker', height: '172', mass: '77'},
{name: 'C-3PO', height: '167', mass: '75'}
]
I should mention I'm using react for this project. My Component is below:
export default function Character(props) {
const [charStats, setCharStats] = useState("");
const [currentName, setCurrentName] = useState("");
const { name } = props;
useEffect(() => {
axios.get(`${BASE_URL}`)
.then(res => {
setCharStats(res.data);
setCurrentName(name);
})
.catch(err => console.error(err))
}, [])
return (
<div>
<div>{ name }</div>
<button>Stats</button>
{ name === currentName ? charStats.map(char => {
return <Stats height={char.height} mass={char.mass} key={char.name} />;
}) : <h3>Loading...</h3>
}
</div>
)
}
The name prop I am getting from another component, I can console.log it and get each individual name so I know that works. But with the code above, I am getting the height and mass of every object returned instead of just the ones that match the name. How can I get specifically the height and mass of each object?
Looks like you might want to call filter before using map, like for example: data.filter(x => x.name === name).map(char => {.... which returns a collection that only contains the elements that match the condition). Or if you only want to find one element, its better to use .find(x => x.name === name) instead

How to map array of objects in React js

I am trying to map the array which I get from API call to the state. Actually it maps elements of array in the correct count. However, I get only 1 st element of array n times(n length of array). Where I did mistake???
export default class NewCalendarView extends Component {
componentDidMount() {
API.getLectures().then((res)=>{
console.log(res)
let cal=res.map((lec)=>{
let lecture={
title: lec.subjectName,
startDate : moment(lec.dateHour).toDate(),
endDate: moment(lec.dateHour).toDate()
}
console.log("lec "+ JSON.stringify(lecture));
return lecture;
})
this.setState({events:cal,loading:null,serverErr:null})
}).catch((err)=>{
this.setState({serverErr:true,loading:null})
})
}
constructor(props) {
super(props);
this.state = {
events: []
}
}
render() {
return (
<div style={{
flex: 1
}}>
<Calendar
localizer={localizer}
events={this.state.events}
startAccessor='startDate'
endAccessor='endDate'
views={['month', 'week', 'day']}
culture='en'
/>
</div>
);
}
}
json from API call
res: [{"subject":"SoftwareEngineering II","date":"2020-11-16","hour":"15:26","modality":"In person","room":"12A","capacity":150,"bookedStudents":100,"teacherName":"Franco yjtyjty","lectureId":1,"booked":false},{"subject":"SoftwareEngineering II","date":"2020-11-14","hour":"17:26","modality":"In person","room":"12A","capacity":50,"bookedStudents":100,"teacherName":"Franco yjtyjty","lectureId":2,"booked":false},{"subject":"SoftwareEngineering II","date":"2020-11-13","hour":"17:26","modality":"In person","room":"12A","capacity":50,"bookedStudents":100,"teacherName":"Franco yjtyjty","lectureId":3,"booked":false},{"subject":"SoftwareEngineering II","date":"2020-11-17","hour":"17:26","modality":"In person","room":"12A","capacity":50,"bookedStudents":100,"teacherName":"Franco yjtyjty","lectureId":4,"booked":false}]
Looks like the keys that you are trying to access do not exist in the res object. Try replacing the keys as defined below and that might help.
It can be confusing to match the parameters in the cloud and app at times especially with camelCase and kebab-case conventions!
let lecture= {
title: lec.subject,
startDate : moment(lec.date).toDate(),
endDate: moment(lec.date).toDate()
}
I would suggest removing this piece of code -
let cal=res.map((lec)=>{
let lecture={
title: lec.subjectName,
startDate : moment(lec.dateHour).toDate(),
endDate: moment(lec.dateHour).toDate()
}
console.log("lec "+ JSON.stringify(lecture));
return lecture;
})
just do this instead -
this.setState({events:JSON.stringy(res),loading:null,serverErr:null})
also I noticed that there is no unique key , this is why its always the first object repeated n times
anyways I noticed Talha Azhar has already answered while I was typing my answer his answer will definitely help , also you can try doing what I suggest above it will also reduce your code .
You're mapping properties that don't exist in your response. You should store the lectureId to use it as key.
I'd also suggest you use a more idiomatic approach, using function components and hooks.
You can rewrite your component like this:
function NewCalendarView() {
const [serverErr, setServerErr] = useState();
const [loading, setLoading] = useState();
const [events, setEvents] = useState();
useEffect(() => {
(async () => {
try {
const lectures = await API.getLectures();
const cal = lectures.map(({ lectureId, subject, date }) => ({
id: lectureId,
title: subject,
startDate: moment(date).toDate(),
endDate: moment(date).toDate()
}));
setEvents(cal);
setServerErr(null);
} catch (e) {
setEvents();
setServerErr(true);
}
setLoading(null);
})();
}, []);
return (
<div
style={{
flex: 1
}}
>
<Calendar
localizer={localizer}
events={events}
startAccessor="startDate"
endAccessor="endDate"
views={["month", "week", "day"]}
culture="en"
/>
</div>
);
}
I put a functional version in this sandbox: https://codesandbox.io/s/react-playground-forked-7sm6h?file=/index.js:2582-3523

react-widgets DropDownList dynamic load on demand

I would like to use the awesome react-widgets DropDownList to load records on demand from the server.
My data load all seems to be working. But when the data prop changes, the DropDownList component is not displaying items, I get a message
The filter returned no results
Even though I see the data is populated in my component in the useEffect hook logging the data.length below.
I think this may be due to the "filter" prop doing some kind of client side filtering, but enabling this is how I get an input control to enter the search term and it does fire "onSearch"
Also, if I use my own component for display with props valueComponent or listComponent it bombs I believe when the list is initially empty.
What am I doing wrong? Can I use react-widgets DropDownList to load data on demand in this manner?
//const ItemComponent = ({item}) => <span>{item.id}: {item.name}</span>;
const DropDownUi = ({data, searching, fetchData}) => {
const onSearch = (search) => {
fetchData(search);
}
// I can see the data coming back here!
useEffect(() => {
console.log(data.length);
}, [data]);
<DropDownList
data={data}
filter
valueField={id}
textField={name}
onSearch={onSearch}
busy={searching} />
};
Got it! This issue is with the filter prop that you are passing to the component. The filter cannot take a true as value otherwise that would lead to abrupt behavior like the one you are experiencing.
This usage shall fix your problem:
<DropdownList
data={state.data}
filter={() => true} // This was the miss/fix 😅
valueField={"id"}
textField={"name"}
busy={state.searching}
searchTerm={state.searchTerm}
onSearch={(searchTerm) => setState({ searchTerm })}
busySpinner={<span className="fas fa-sync fa-spin" />}
delay={2000}
/>
Working demo
The entire code that I had tried at codesandbox:
Warning: You might have to handle the clearing of the values when the input is empty.
I thought that the logic for this was irrelevant to the problem statement. If you want, I can update that as well.
Also, I added a fakeAPI when searchTerm changes that resolves a mocked data in 2 seconds(fake timeout to see loading state).
import * as React from "react";
import "./styles.css";
import { DropdownList } from "react-widgets";
import "react-widgets/dist/css/react-widgets.css";
// Coutesy: https://usehooks.com/useDebounce
import useDebounce from "./useDebounce";
interface IData {
id: string;
name: string;
}
const fakeAPI = () =>
new Promise<IData[]>((resolve) => {
window.setTimeout(() => {
resolve([
{
name: "NA",
id: "user210757"
},
{
name: "Yash",
id: "id-1"
}
]);
}, 2000);
});
export default function App() {
const [state, ss] = React.useState<{
searching: boolean;
data: IData[];
searchTerm: string;
}>({
data: [],
searching: false,
searchTerm: ""
});
const debounceSearchTerm = useDebounce(state.searchTerm, 1200);
const setState = (obj: Record<string, any>) =>
ss((prevState) => ({ ...prevState, ...obj }));
const getData = () => {
console.log("getting data...");
setState({ searching: true });
fakeAPI().then((response) => {
console.log("response: ", response);
setState({ searching: false, data: response });
});
};
React.useEffect(() => {
if (debounceSearchTerm) {
getData();
}
}, [debounceSearchTerm]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<DropdownList
data={state.data}
filter={() => true} // This was the miss/fix 😅
valueField={"id"}
textField={"name"}
busy={state.searching}
searchTerm={state.searchTerm}
onSearch={(searchTerm) => setState({ searchTerm })}
busySpinner={<span className="fas fa-sync fa-spin" />}
delay={2000}
/>
</div>
);
}
Let me know if you have more queries on this 😇
So it i think that list should be loaded a then you can filtering your loaded data.In your example on the beginning you don't have value so list is empty, you tape in some text and then value of list re render but it look like is not filtered.....
However I look through code base, and it's look like is not ready until you don't set manually open prop drop down list component. In getDerivedStateFromprops, next data list is read only if in next props is open set. to true
From DropDwonList
static getDerivedStateFromProps(nextProps, prevState) {
let {
open,
value,
data,
messages,
searchTerm,
filter,
minLength,
caseSensitive,
} = nextProps
const { focusedItem } = prevState
const accessors = getAccessors(nextProps)
const valueChanged = value !== prevState.lastValue
let initialIdx = valueChanged && accessors.indexOf(data, value)
//-->> --- -- --- -- -- -- -- - - - - - - - - - --- - - --------
//-->>
if (open)
data = Filter.filter(data, {
filter,
searchTerm,
minLength,
caseSensitive,
textField: accessors.text,
})
const list = reduceToListState(data, prevState.list, { nextProps })
const selectedItem = data[initialIdx]
const nextFocusedItem = ~data.indexOf(focusedItem) ? focusedItem : data[0]
return {
data,
list,
accessors,
lastValue: value,
messages: getMessages(messages),
selectedItem: valueChanged
? list.nextEnabled(selectedItem)
: prevState.selectedItem,
focusedItem:
(valueChanged || focusedItem === undefined)
? list.nextEnabled(selectedItem !== undefined ? selectedItem : nextFocusedItem)
: nextFocusedItem,
}
}
I would try:
<DropDownList
data={data}
filter
open
valueField={id}
textField={name}
onSearch={onSearch}
busy={searching} />
};
if it will be works, then you just have to
manage your open state by yourself.

Categories

Resources