I am just learning to program and am writing one of my first applications in React. I am having trouble with an unexpected mutation that I cannot find the roots of. The snippet is part of a functional component and is as follows:
const players = props.gameList[gameIndex].players.map((item, index) => {
const readyPlayer = [];
props.gameList[gameIndex].players.forEach(item => {
readyPlayer.push({
id: item.id,
name: item.name,
ready: item.ready
})
})
console.log(readyPlayer);
readyPlayer[index].test = "test";
console.log(readyPlayer);
return (
<li key={item.id}>
{/* not relevant to the question */}
</li>
)
})
Now the problem is that readyPlayer seems to be mutated before it is supposed to. Both console.log's read the same exact thing. That is the array with the object inside having the test key as "test". forEach does not mutate the original array, and all the key values, that is id, name and ready, are primitives being either boolean or string. I am also not implementing any asynchronous actions here, so why do I get such an output? Any help would be greatly appreciated.
Below is the entire component for reference in its original composition ( here also the test key is replaced with the actual key I was needing, but the problem persists either way.
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
// import styles from './Lobby.module.css';
const Lobby = ( props ) => {
const gameIndex = props.gameList.findIndex(item => item.id === props.current.id);
const isHost = props.gameList[gameIndex].hostId === props.user.uid;
const players = props.gameList[gameIndex].players.map((item, index) => {
const isPlayer = item.id === props.user.uid;
const withoutPlayer = [...props.gameList[gameIndex].players];
withoutPlayer.splice(index, 1);
const readyPlayer = [];
props.gameList[gameIndex].players.forEach(item => {
readyPlayer.push({
id: item.id,
name: item.name,
ready: item.ready
})
})
const isReady = readyPlayer[index].ready;
console.log(readyPlayer);
console.log(!isReady);
readyPlayer[index].ready = !isReady;
console.log(readyPlayer);
return (
<li key={item.id}>
{isHost && index !== 0 && <button onClick={() => props.updatePlayers(props.gameList[gameIndex].id, withoutPlayer)}>Kick Player</button>}
<p>{item.name}</p>
{isPlayer && <button onClick={() =>props.updatePlayers(props.gameList[gameIndex].id, readyPlayer)}>Ready</button>}
</li>
)
})
let showStart = props.gameList[gameIndex].players.length >= 2;
props.gameList[gameIndex].players.forEach(item => {
if (item.ready === false) {
showStart = false;
}
})
console.log(showStart);
return (
<main>
<div>
{showStart && <Link to="/gameboard" onClick={props.start}>Start Game</Link>}
<Link to="/main-menu">Go back to Main Menu</Link>
</div>
<div>
<h3>Players: {props.gameList[gameIndex].players.length}/4</h3>
{players}
</div>
</main>
);
}
Lobby.propTypes = {
start: PropTypes.func.isRequired,
current: PropTypes.object.isRequired,
gameList: PropTypes.array.isRequired,
updatePlayers: PropTypes.func.isRequired,
user: PropTypes.object.isRequired
}
export default Lobby;
Note: I did manage to make the component actually do what it is supposed, but the aforementioned unexpected mutation persists and is still a mystery to me.
I have created a basic working example using the code snippet you provided. Both console.log statements return a different value here. The first one returns readyPlayer.test as undefined, the second one as "test". Are you certain that the issue happens within your code snippet? Or am I missing something?
(Note: This answer should be a comment, but I am unable to create a code snippet in comments.)
const players = [
{
id: 0,
name: "John",
ready: false,
},
{
id: 1,
name: "Jack",
ready: false,
},
{
id: 2,
name: "Eric",
ready: false
}
];
players.map((player, index) => {
const readyPlayer = [];
players.forEach((item)=> {
readyPlayer.push({
id: item.id,
name: item.name,
ready: item.ready
});
});
// console.log(`${index}:${readyPlayer[index].test}`);
readyPlayer[index].test = "test";
// console.log(`${index}:${readyPlayer[index].test}`);
});
console.log(players)
Related
I'm using React Hook Form to build a basic page builder application and it's been brilliant so far, I've been using the useFieldArray hook to create lists that contain items, however, I haven't found a way to move items between lists.
I know I can use the move() function to reorder items within the same list, however, since each list has its own nested useFieldArray I can't move the item from one list component to another list component.
If anyone knows of a way around this it would be much appreciated!
Here is a very simplified example of my current setup:
export const App = () => {
const methods = useForm({
defaultValues: {
lists: [
{
list_id: 1,
items: [
{
item_id: 1,
name: 'Apple'
},
{
item_id: 2,
name: 'Orange'
}
]
},
{
list_id: 2,
items: [
{
item_id: 3,
name: 'Banana'
},
{
item_id: 4,
name: 'Lemon'
}
]
}
]
}
});
return (
<FormProvider {...methods}>
<Page/>
</FormProvider>
)
}
export const Page = () => {
const { control } = useFormContext();
const { fields } = useFieldArray({
control,
name: 'lists'
})
return (
<ul>
{fields?.map((field, index) => (
<List listIdx={index} />
))}
</ul>
)
}
export const List = ({ listIdx }) => {
const { control, watch } = useFormContext();
const { fields, move } = useFieldArray({
control,
name: `lists[${sectionIdx}].items`
})
const handleMove = (prevIdx, nextIdx) => {
// this allows me to move within lists but not between them
move(prevIdx, nextIdx);
}
return (
<li>
<p>ID: {watch(lists[${listIdx}].list_id)}</p>
<ul>
{fields?.map((field, index) => (
<Item listIdx={index} itemIdx={index} handleMove={handleMove}/>
))}
</ul>
</li>
)
}
export const Item = ({ listIdx, itemIdx, handleMove }) => {
const { control, register } = useFormContext();
return (
<li>
<p>ID: {watch(lists[${listIdx}].items[${itemIdx}].item_id)}</p>
<label
Name:
<input { ...register('lists[${listIdx}].items[${itemIdx}]) }/>
/>
<button onClick={() => handleMove(itemIdx, itemIdx - 1)}>Up</button>
<button onClick={() => handleMove(itemIdx, itemIdx + 1)}>Down</button>
</div>
)
}
Thanks in advance!
If you'd not like to alter your default values (your data structure), I think the best way to handle this is using update method returning from useFieldArray. You have the data of both inputs that are going to be moved around, knowing their list index and item index, you could easily update their current positions with each other's data.
Hello I am struggling to properly update my state from my child component to my parent.
Basically I am trying to set the current state to true onclick.
This is my parent component:
export default function Layout({ children }: Props) {
const [navigation, setNavigation] = useState([
{ name: 'Dashboard', href: '/', icon: HomeIcon, current: true },
{ name: 'Create Fact', href: '/facts/create', icon: UsersIcon, current: false },
{ name: 'Documents', href: '/documents', icon: InboxIcon, current: false }
])
return (
<>
<Sidebar navigation={navigation} setNavigation={setNavigation} />
This is my child Component (Sidebar)
type Props = {
navigation: Array<{
name: string
href: string
icon: any
current: boolean
}>
setNavigation: (
navigation: Array<{
name: string
href: string
icon: any
current: boolean
}>
) => void
}
const Sidebar = ({navigation, setNavigation}: Props) => {
const router = useRouter()
const toggleNavigation = (name: string) => {
// todo: Here I would like to properly update the state with the current selected navigation item (current)
const newNavigation = navigation.map(nav => {
if (nav.name === name) {
nav.current = true
return nav
}
})
}
return (
<nav className="flex-1 px-2 pb-4 space-y-1">
{navigation.map(item => (
<span
onClick={() => toggleNavigation(item.name)}
There are three problems:
You never call setNavigation with your new array.
You don't clear current on the formerly-current item.
Although you're creating a new array, you're reusing the objects within it, even when you change them, which is against the Do Not Modify State Directly rule.
To fix all three (see *** comments):
const toggleNavigation = (name: string) => {
const newNavigation = navigation.map(nav => {
if (nav.name === name) {
// *** #3 Create a *new* object with the updated state
nav = {...nav, current: true};
} else if (nav.current) { // *** #2 make the old current no longer current
nav = {...nav, current: false};
}
return nav;
});
// *** #1 Do the call to set the navigation
setNavigation(newNavigation);
};
Separately, though, I would suggest separating navigation out into two things:
The set of navigation objects.
The name of the current navigation item.
Then setting the navigation item is just setting a new string, not creating a whole new array with an updated object in it.
T.J. Crowder's solution and explanation are great.
Additionally, you can write that logic in a shorter syntax. Just a preference.
const newNavigation = navigation.map(nav => {
return nav.name === name
? { ...nav, current: true }
: { ...nav, current: false }
})
Right now I'm building a DnD game.
It looks like this:
When I drop an answer over a slot, the answer should be in that exact place, so the order matters.
My approach kind of works, but not perfectly.
So, in the parent component I have two arrays which store the slots' content and the answers' content.
The problem is easy to understand, you don't have to look at the code in-depth, just mostly follow my text :)
import DraggableGameAnswers from './DraggableGameAnswers';
import DraggableGameSlots from './DraggableGameSlots';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useState } from 'react';
type DraggableGameStartProps = {
gameImages: Array<string>,
gameAnswers: Array<string>,
numberOfGameAnswers?: number,
typeOfAnswers: string,
correctAnswer: string
}
function DraggableGameStart(props: DraggableGameStartProps) {
const [slots, setSlots] = useState<string[]>(["", "", "", ""]);
const [answers, setAnswers] = useState<string[]>(props.gameAnswers);
return (
<DndProvider backend={HTML5Backend}>
<div className="draggable-game-content">
<div className="draggable-game">
<DraggableGameSlots
answers={answers}
slots={slots}
setAnswers={setAnswers}
setSlots={setSlots}
numberOfAnswers={4}
slotFor={props.typeOfAnswers}
/>
<DraggableGameAnswers
gameAnswers={answers}
numberOfGameAnswers={props.numberOfGameAnswers}
typeOfAnswers={props.typeOfAnswers}
/>
</div>
</div>
</DndProvider>
);
}
export default DraggableGameStart;
DraggableGameSlots is then a container, which loops through props.slots and renders each slot.
import React, { useState } from "react";
import DraggableGameSlot from "./DraggableGameSlot";
type DraggableGameSlotsProps = {
answers: string[],
slots: string[],
setAnswers: any,
setSlots: any,
numberOfAnswers: number,
slotFor: string
}
function DraggableGameSlots(props: DraggableGameSlotsProps) {
return (
<div className="draggable-game-slots">
{
props.slots.map((val, index) => (
<DraggableGameSlot
index={index}
typeOf={props.slotFor === "text" ? "text" : "image"}
key={index}
answers={props.answers}
slots={props.slots}
setAnswers={props.setAnswers}
setSlots={props.setSlots}
toDisplay={val === "" ? "Drop here" : val}
/>
))
}
</div>
);
}
export default DraggableGameSlots;
In DraggableGameSlot I make my slots a drop target. When I drop an element, I iterate through the slots, and when I reach that element's position in the array, I modify it with the answer.
Until now everything works fine, as expected, I just wrote this for context.
E.g. if the drop target's index is 2, I modify the 3rd position in the slots array. (slots2 for 0-index based)
import { useEffect } from 'react';
import { useDrop } from 'react-dnd';
import './css/DraggableGameSlot.css';
import DraggableGameImage from './DraggableGameImage';
type DraggableGameSlotProps = {
index: number,
typeOf: string,
answers: string[],
slots: string[],
setAnswers: any,
setSlots: any,
toDisplay: string
}
function DraggableGameSlot(props: DraggableGameSlotProps) {
const [{ isOver }, drop] = useDrop(() => ({
accept: "image",
drop(item: { id: string, toDisplay: string }) {
props.setAnswers(props.answers.filter((val, index) => index !== parseInt(item.id)));
props.setSlots(props.slots.map((val, index) => {
if (props.index === index) {
console.log("ITEM " + item.toDisplay);
return item.toDisplay;
}
return val;
}));
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
})
}), [props.slots])
// useEffect(() => console.log("answers " +props.answers), [props.answers]);
// useEffect(() => console.log("slots " + props.slots), [props.slots]);
const dragClass: string = isOver ? "is-dragged-on" : "";
return (
<>
{(props.typeOf === "image" && props.toDisplay !== "Drop here") ?
<>
<span>da</span>
<DraggableGameImage
className={`game-answers-image ${dragClass}`}
imgSrc={props.toDisplay}
imgAlt={"test"}
dndRef={drop}
/>
</>
:
<div className={`draggable-game-slot draggable-game-slot-for-${props.typeOf} ${dragClass}`} ref={drop}>
<span>{props.toDisplay}</span>
</div>
}
</>
)
}
export default DraggableGameSlot;
The problem comes with the logic in DraggableGameAnswer.
First, the container, DraggableGameAnswer. Using the loop, I pass that "type" (it is important because from there the error happens)
DraggableGameAnswers.tsx
import './css/DraggableGameAnswers.css';
import GameNext from '../GameComponents/GameNext';
import DraggableGameAnswer from './DraggableGameAnswer';
import { useEffect } from 'react';
type DraggableGameAnswersProps = {
gameAnswers: Array<string>,
numberOfGameAnswers?: number,
typeOfAnswers: string
}
function DraggableGameAnswers(props: DraggableGameAnswersProps) {
let toDisplay: Array<string>;
if(props.numberOfGameAnswers !== undefined)
{
toDisplay = props.gameAnswers.slice(props.numberOfGameAnswers);
}
else
{
toDisplay = props.gameAnswers;
}
useEffect(() => console.log(toDisplay));
return (
<div className="draggable-game-answers">
{
toDisplay.map((val, index) => (
<DraggableGameAnswer
key={index}
answer={val}
typeOfAnswer={props.typeOfAnswers}
type={`answer${index}`}
/>
))}
<GameNext className="draggable-game-verify" buttonText="Verify"/>
</div>
);
}
export default DraggableGameAnswers;
In DraggableGameAnswer, I render the draggable answer:
import DraggableGameImage from "./DraggableGameImage";
import "./css/DraggableGameAnswer.css";
import { useDrag } from "react-dnd";
import { ItemTypes } from "./Constants";
type DraggableGameAnswerProps = {
answer: string,
typeOfAnswer: string,
type: string
}
function DraggableGameAnswer(props: DraggableGameAnswerProps) {
const [{ isDragging }, drag] = useDrag(() => ({
type: "image",
item: { id: ItemTypes[props.type],
toDisplay: props.answer,
typeOfAnswer: props.typeOfAnswer },
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
})
}))
return (
<>
{props.typeOfAnswer === "image" ?
<DraggableGameImage
className="draggable-game-image-answer"
imgSrc={props.answer}
imgAlt="test"
dndRef={drag}
/> :
<div className="draggable-game-text-answer" ref={drag}>
{props.answer}
</div>
}
</>
);
}
export default DraggableGameAnswer;
Constants.tsx (for ItemTypes)
export const ItemTypes: Record<string, any> = {
answer0: '0',
answer1: '1',
answer2: '2',
answer3: '3'
}
Okay, now let me explain what's happening.
The error starts because of the logic in DraggableGameAnswers container - I set the type using the index for the loop.
Everything works fine at the beginning. Then, let's say, I move answer3 in in the 4th box. It looks like this now:
It is working fine until now.
But now, the components have re-rendered, the map runs again, but the array is shorter with 1 and answer1's type will be answer1, answer2's type will be answer2, and answer4's type will be answer3.
So, when I drag answer4 over any box, I will get another answer3, like this:
How can I avoid this behavior and still get answer4 in this situation? I'm looking for a react-style way, better than my idea that I'll describe below.
So, here is how I delete an answer from the answers array:
props.setAnswers(props.answers.filter((val, index) => index !== parseInt(item.id)));
I can just set the deleted's answer position to NULL, to keep my array length for that index, and when I render the answers if the value is NULL, just skip it.
I am learning React and I think I am missing something fundamental with updating the state / rendering components.
const allFalse = new Array(data.length)
const allTrue = new Array(data.length)
allFalse.fill(false)
allTrue.fill(true)
const [memoryStatus, setMemoryStatus] = useState(allFalse)
const [baseValue, setBaseValue] = useState(false)
The memory game has 5 cards at this point (just learning here) and depending on the memoryStatus it is determined if one side or other side is shown (true / false).
When clicked on a card I obviously want to change the value of that card in the array. I am doing that with this function:
const handleChange = (position) => {
const newMemoryStatus = memoryStatus.map((item, index) =>
{
if(index === position) {
return !item
}
else return item
}
)
// i really dont understand why this does not change the state
setMemoryStatus[newMemoryStatus]
}
The render part is:
<div className={styles.container}>
{data.map((item, index) => {
return (
<div
key={index}
onClick={() => {handleChange(index)}}
className={styles.card}
>
{!memoryStatus[index] && <Image
src={item.img}
width="100px"
height="100px"
/>}
<span>
<center>
{memoryStatus[index] ? item.latinName : ''}
</center>
</span>
</div>
)})
}
</div>
Just in case it matters my data looks like this:
const data = [
{
name: 'Staande geranium',
latinName: 'Pelargonium zonate',
img: '/../public/1.png'
},
{
name: 'Groot Afrikaantje',
latinName: 'Tagetes Erecta',
img: '/../public/2.png'
},
{
name: 'Vuursalie',
latinName: 'Salvia splendens',
img: '/../public/3.png'
},
{
name: 'Kattenstaart',
latinName: 'Amaranthus caudatus',
img: '/../public/4.png'
},
{
name: 'Waterbegonia',
latinName: 'Begonia semperflorens',
img: '/../public/5.png'
}]
What am I doing wrong ??
setMemoryStatus is a function, thus you should be using parentheses () instead of brackets [] when calling it. The line to call it should be:
setMemoryStatus(newMemoryStatus);
I'm trying to map through my state, where I have collected data from an external API.
However, when I do i get this error here:
TypeError: this.state.stocks.map is not a function
I want to render the results to the frontend, through a function so that the site is dynamic to the state.favorites.
Though the console.log(), I can see that the data is stored in the state.
I have found others with a similar issue, but the answers did not work out.
UPDATE:
I have changed the componentDidMount() and it now produces an array. The issue is that I get no render from the renderTableData() functions.
console.log shows this array:
0: {symbol: "ARVL", companyName: "Arrival", primaryExchange: "AESMSBCDA)LNKOS/ TLTE(N GAEQLGAR ", calculationPrice: "tops", open: 0, …}
1: {symbol: "TSLA", companyName: "Tesla Inc", primaryExchange: " RNK EAASGTDACLN)LE/OGMELAQSTB (S", calculationPrice: "tops", open: 0, …}
2: {symbol: "AAPL", companyName: "Apple Inc", primaryExchange: "AMTGS/C) AALGDRSTNLEOEL(S BAE NQK", calculationPrice: "tops", open: 0, …}
length: 3
__proto__: Array(0)
This is my code:
import React, { Component } from 'react'
import './Table.css';
class Table extends Component {
constructor (props) {
super(props);
this.state = {
favorites: ['aapl', 'arvl', 'tsla'],
stocks: []
};
}
componentDidMount() {
this.state.favorites.map((favorites, index) => {
fetch(`API`)
.then((response) => response.json())
.then(stockList => {
const stocksState = this.state.stocks;
const stockListValObj = stockList;
console.log(stocksState)
console.log(stockListValObj)
this.setState({
stocks: [
... stocksState.concat(stockListValObj)
]
}, () => { console.log(this.state.stocks);});
})
})
}
renderTableData() {
this.state.stocks.map((stocks, index) => {
const { companyName, symbol, latestPrice, changePercent, marketCap } = stocks //destructuring
return (
<div key={symbol} className='headers'>
<div className='first-value'>
<h4>{companyName}</h4>
<h4 className='symbol'>{symbol}</h4>
</div>
<div className='align-right'>
<h4>{latestPrice}</h4>
</div>
<div className='align-right'>
<h4 className='changePercent'>{changePercent}</h4>
</div>
<div className='align-right'>
<h4>{marketCap}</h4>
</div>
</div>
);
})
}
render() {
return (
<div className='table'>
<h1 id='title'>Companies</h1>
<div className='headers'>
<h4 className='align-right'></h4>
<h4 className='align-right'>Price</h4>
<h4 className='align-right'>+/-</h4>
<h4 className='align-right'>Market Cap</h4>
</div>
<div>
{this.renderTableData()}
</div>
</div>
)
}
}
export default Table;
You should always aim in making single state update, try reducing the state update to single update.
I suggest 2 solution:
Move the data fetch section into a separate function, update a temporary array variable return the variable at the end of the execution.
async dataFetch() {
const sampleData = this.state.stocks || [];
await this.state.favorites.forEach((favorites, index) => {
fetch(`API`)
.then((response) => response.json())
.then((stockList) => {
sampleData.push(stockList);
// const stocksState = this.state.stocks;
// const stockListValObj = stockList;
// console.log(stocksState);
// console.log(stockListValObj);
// this.setState({
// stocks: [
// ... stocksState.concat(stockListValObj)
// ]
// }, () => { console.log(this.state.stocks);});
});
});
return Promise.resolve(sampleData);
}
componentDidMount() {
this.dataFetch().then((stockValues) => {
this.setState({ stocks: stockValues });
});
}
Use Promise.all() this return a single array value which would be easier to update on to the stock state.
Suggestion : When not returning any values from the array try using Array.forEach instead of Array.map
Return keyword is missing in renderTableData
renderTableData() {
this.state.stocks.map((stocks, index) => { ...});
to
renderTableData() {
return this.state.stocks.map((stocks, index) => { ...});
I would say that this is happening because on the first render, the map looks into state.stock and it's an empty array. After the first render, the componentDidMount method is called fetching the data.
I would suggest to just wrap the map into a condition. If stock doesn't have any object, then return whatever you wish (null or a loader/spinner for example).
It's enough to add it like this for not returning anything in case the array is empty (it will be filled after the first render, but this is useful as well to return error message in case the fetch fails):
this.state.stocks.length > 0 && this.state.stocks.map((stocks, index) => {