Creating an array in one function and then using it somewhere else - javascript

TL:DR, I am creating a randomly-ordered array in one React component, that I need to use in another component - but the second component keeps re-rendering and therefore re-shuffling my array - but I need its order to be fixed once it gets imported for the first time.
First things first - if I am doing this in a needlessly roundabout way, please do say so, I'm not set on this way.
I am making a flashcard program, and I want to give users the option to play games with random selections of their cards.
The way I am currently trying to do this, is that I have a functional component (because I need to do things like dispatch in it) which works as follows - I've added comments to explain what each bit does:
import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getPosts } from "../../actions/posts";
export function WORDS() {
const dispatch = useDispatch();
const [localUser, setLocalUser] = useState();
//get all users
useEffect(() => {
dispatch(getPosts());
}, []);
const users = useSelector((state) => state.posts);
//set user to match the user in the local storage
const [user, setUser] = useState();
useEffect(() => {
setLocalUser(JSON.parse(localStorage.getItem("profile")));
}, [localStorage]);
useEffect(() => {
setUser(users.filter((user) => user.code == localUser?.code)[0]);
}, [users, localUser]);
//create an array of 8 random words from the users cards object
let RandomArray = []
if (user) {
for (let i = 0; RandomArray.length < 8; i++) {
let RanNum = Math.floor(Math.random() * user.cards.length);
!RandomArray.includes(user.cards[RanNum]) && RandomArray.push(user.cards[RanNum])
}
}
//create duplicates of each word and make an array of them all, once with the front of the card in first place and once with the back in first place
let shuffledWords = [];
RandomArray.map((word) => {
let newWord = { Q: word.front, A: word.back };
let newWord2 = { Q: word.back, A: word.front };
shuffledWords.push(newWord);
shuffledWords.push(newWord2);
});
//shuffle that array
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
shuffleArray(shuffledWords);
//return this array so I can call it in other functions
return { shuffledWords }
};
The game is then a kind of 'memory' game, where users try and match words with their translation. That's currently running like this, again with comments (please excuse the excessive inline styling - I will move that all to the stylesheet eventually but I've left it in here just in case any of that is what's causing the problems.):
import React, { useState } from "react";
import { WORDS as ImportedWords } from "./WORDS";
export const Memory = () => {
//import words from function
const ImportedArray = ImportedWords().shuffledWords
//create state variables for the first and second card a user pics
const [selectL1, setSelectL1] = useState("");
const [selectL2, setSelectL2] = useState("");
//create state variables for whether to display a card or not and whether or not it was correct
const [show, setShow] = useState([]);
const [correct, setCorrect] = useState([]);
//variable to make cards unclickable while they're spinning over
const [clickable, setClickable] = useState(true);
//if user has picked two cards, check if they match and either set them to correct or turn them back over
if (selectL1 && selectL2) {
clickable == true && setClickable(false);
let selectQ = ImportedArray.filter((word) => word.Q == selectL1)[0];
console.log("selectQ: ", selectQ);
selectQ && selectL2 == selectQ.A
? correct.push(selectL1, selectL2)
: console.log("Incorrect!");
setTimeout(function () {
setSelectL1("");
setSelectL2("");
setShow([]);
setClickable(true);
//correct.length == shuffledWords.length * 2 && alert("You win!");
}, 800);
}
//show the card a user's clicked
const selectCard = (wordId) => {
!selectL1 ? setSelectL1(wordId) : setSelectL2(wordId);
show.push(wordId);
};
return (
<div className="memory-game-wrapper">
<div
style={{ perspective: "2000px", pointerEvents: !clickable && "none" }}
>
{/* filter through the imported array and display them */}
{ImportedArray.map((word) => {
return (
<div
className={
show.includes(word.Q) || correct.includes(word.Q)
? "card flip"
: "card"
}
id={word.Q}
style={{
borderRadius: "5px",
display: "inline-block",
width: "100px",
height: "180px",
backgroundColor: "rgb(185, 204, 218)",
margin: "5px",
}}
onClick={() =>
!correct.includes(word.Q) &&
!show.includes(word.Q) &&
selectCard(word.Q)
}
>
<div
className="back-face"
style={{
position: "absolute",
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<center>
<span style={{ userSelect: "none" }}></span>
</center>
</div>
<div
className="front-face"
style={{
position: "absolute",
height: "100%",
display: "flex",
width: "100%",
borderRadius: "5px",
border: "5px solid rgb(185, 204, 218)",
boxSizing: "border-box",
backgroundColor: correct.includes(word.Q)
? "white"
: "rgb(185, 204, 218)",
justifyContent: "center",
alignItems: "center",
}}
>
<h3 style={{ userSelect: "none" }}>{word.Q}</h3>
</div>
</div>
);
})}
</div>
</div>
);
};
I suspected that what was happening was that the whole array is being re-rendered any time a user clicks on any of the cards, which means the order gets shuffled again - and makes the game unplayable, so I whacked in a console.log(ImportedArray[0]) to check and yes, that is definitely happening. But I can't work out how to stop it?
Any ideas??

If the Memory component is not conditionally mounted/unmounted in the parent, like {condition && <Memory}, you can use the useMemo hook to memoize the imported words at the first render.
const ImportedArray = useMemo(() => ImportedWords().shuffledWords, []);
Anyway the WORDS component is a candidate to be a custom hook where you can encapsulate the words logic. it should be named useWords

Related

Grid Size isn't changing properly in React when changing state

I'm trying to change the grid size on my Conway game of life app but when I click button to set new numCols/numRows only one of them is effected on the app. How do I affectively set new state so grid changes size as expected.
I have 2 buttons in the app, one to make grid smaller, one to make it bigger.
onClick they Trigger function sizeHandler & sizeHandler2.
My guess is I need to set new state in a different method but I tried a few methods to no avail.
import React, { useState } from 'react'
import './App.css';
function App() {
const color = "#111"
const [numRows, setNumRows] = useState(20)
const [numCols, setNumCols] = useState(20)
const generateEmptyGrid = () => {
const rows = [];
for (let i = 0; i < numRows; i++) {
rows.push(Array.from(Array(numCols), () => 0))
}
return rows
}
const [grid, setGrid] = useState(() => {
return generateEmptyGrid();
})
const sizeHandler = () => {
setNumRows(40)
setNumCols(40)
}
const sizeHandler2 = () => {
setNumRows(20)
setNumCols(20)
}
// RENDER
return (
<div className="page">
<div className="title">
<h1>
Conway's Game Of Life
</h1>
<button onClick={sizeHandler}>
Size+
</button>
<button onClick={sizeHandler2}>
Size-
</button>
</div>
<div className="grid" style={{
display: 'grid',
gridTemplateColumns: `repeat(${numCols}, 20px)`
}}>
{grid.map((rows, i) =>
rows.map((col, j) =>
<div className="node"
key={`${i}-${j}`}
style={{
width: 20,
height: 20,
border: 'solid 1px grey',
backgroundColor: grid[i][j] ? color : undefined
}}
/>
))}
</div>
</div>
);
}
export default App;
What you are doing is you want a change of numRows and numCols to have a side effect. You actually almost said it yourself. React has a hook for that: useEffect:
useEffect(() => {
setGrid(generateEmptyGrid())
}, [numRows, numCols]);

React js - Array doesn't update after a onSwipe() event

I am a Beginner to Reactjs and I just started working on a Tinder Clone with swipe functionality using tinde-card-react.
I am trying to get two variables to update using React useState() but coudn't.
There are 2 main components inside the main function, a TinderCards component and Swipe right and left and Replay buttons. The problem is that when I swipe the cards manually variables don't get updated and this is not the case when i swipe using the buttons.
In the current log, I swiped the cards twice to the right and logged the variables alreadyRemoved and people. The variable people is initially an Array containing 3 objects so after the second swipe it's supposed to log only 2 objects not 3, While the alreadyRemoved variable is supposed to update to the missing elements of the variable people.
This is my code :
import React, { useState, useEffect, useMemo } from 'react';
import './IslamCards.css';
import Cards from 'react-tinder-card';
import database from './firebase';
import hate from "./Cross.png"
import replayb from "./Replay.png"
import love from "./Love.png"
import IconButton from "#material-ui/core/IconButton"
function IslamCards(props) {
let [people, setPeople] = useState([])
useEffect(() => {
database.collection("People").onSnapshot(snapshot => { setPeople(snapshot.docs.map(doc => doc.data())) })
}, [])
let [alreadyRemoved , setalreadyRemoved] = useState([])
let buttonClicked = "not clicked"
// This fixes issues with updating characters state forcing it to use the current state and not the state that was active when the card was created.
let childRefs = useMemo(() => Array(people.length).fill(0).map(() => React.createRef()), [people.length])
let swiped = () => {
if(buttonClicked!=="clicked"){
console.log("swiped but not clicked")
if(people.length){
let cardsLeft = people.filter(person => !alreadyRemoved.includes(person))
if (cardsLeft.length) {
let toBeRemoved = cardsLeft[cardsLeft.length - 1] // Find the card object to be removed
let index = people.map(person => person.name).indexOf(toBeRemoved.name)// Find the index of which to make the reference to
setalreadyRemoved(list => [...list, toBeRemoved])
setPeople(people.filter((_, personIndex) => personIndex !== index))
console.log(people)
console.log(alreadyRemoved)
}
}
buttonClicked="not clicked"
}
}
let swipe = (dir) => {
buttonClicked="clicked"
console.log("clicked but not swiped")
if(people.length){
let cardsLeft = people.filter(person => !alreadyRemoved.includes(person))
if (cardsLeft.length) {
let toBeRemoved = cardsLeft[cardsLeft.length - 1] // Find the card object to be removed
let index = people.map(person => person.name).indexOf(toBeRemoved.name)// Find the index of which to make the reference to
setalreadyRemoved(list => [...list, toBeRemoved])
childRefs[index].current.swipe(dir)
let timer =setTimeout(function () {
setPeople(people.filter((_, personIndex) => personIndex !== index))}
, 1000)
console.log(people)
console.log(alreadyRemoved)
}
// Swipe the card!
}
}
let replay = () => {
let cardsremoved = alreadyRemoved
console.log(cardsremoved)
if (cardsremoved.length) {
let toBeReset = cardsremoved[cardsremoved.length - 1] // Find the card object to be reset
console.log(toBeReset)
setalreadyRemoved(alreadyRemoved.filter((_, personIndex) => personIndex !== (alreadyRemoved.length-1)))
if (!alreadyRemoved.length===0){ alreadyRemoved=[]}
let newPeople = people.concat(toBeReset)
setPeople(newPeople)
// Make sure the next card gets removed next time if this card do not have time to exit the screen
}
}
return (
<div>
<div className="cardContainer">
{people.map((person, index) => {
return (
<Cards ref={childRefs[index]} onSwipe={swiped} className="swipe" key={index} preventSwipe={['up', 'down']}>
<div style={{ backgroundImage: `url(${person.url})` }} className="Cards">
<h3>{person.name}</h3>
</div>
</Cards>);
})}
</div>
<div className="reactionButtons">
<IconButton onClick={() => swipe('left')}>
<img id="hateButton" alt="d" src={hate} style={{ width: "10vh", marginBottom: "5vh", pointerEvents: "all" }} />
</IconButton>
<IconButton onClick={() => replay()}>
<img id="replayButton" alt="e" src={replayb} style={{ width: "11vh", marginBottom: "5vh", pointerEvents: "all" }} />
</IconButton>
<IconButton onClick={() => swipe('right')}>
<img id="loveButton" alt="f" src={love} style={{ width: "11vh", marginBottom: "5vh", pointerEvents: "all" }} />
</IconButton>
</div>
</div>
);
}
export default IslamCards;
My console Log :
UPDATE :
As suggested in the 1st answer, I removed the Timer from the swiped() function but the problem persisted.
I hope to get more suggestions, so that I can solve this problem.
I can see the problem, but you might need to figure out what to do after that.
setPeople(people.filter((_, personIndex) => personIndex !== index))}
, 1000)
The problem is that index is figured out from the current update, however it takes 1 second to reach the next update, in between, your index points to the same one, because your index is derived from the people.

Conditionally render part of object onClick inside a map (REACT.js)

I am trying to conditionally render part of an object (user comment) onClick of button.
The objects are being pulled from a Firebase Database.
I have multiple objects and want to only render comments for the Result component I click on.
The user comment is stored in the same object as all the other information such as name, date and ratings.
My original approach was to set a boolean value of false to each Result component and try to change this value to false but cannot seem to get it working.
Code and images attached below, any help would be greatly appreciated.
{
accumRating: 3.7
adheranceRating: 4
cleanRating: 2
date: "2020-10-10"
place: "PYGMALIAN"
staffRating: 5
timestamp: t {seconds: 1603315308, nanoseconds: 772000000}
userComment: "Bad"
viewComment: false
}
const results = props.data.map((item, index) => {
return (
<div className='Results' key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={'read-only'}
value={item.accumRating}
style={{
width: 'auto',
alignItems: 'center',
}}
/>
<button>i</button>
{/* <span>{item.userComment}</span> */}
</div >
)
})
You have to track individual state of each button toggle in that case.
The solution I think of is not the best but you could create a click handler for the button and adding a classname for the span then check if that class exists. If it exists then, just hide the comment.
Just make sure that the next sibling of the button is the target you want to hide/show
const toggleComment = (e) => {
const sibling = e.target.nextElementSibling;
sibling.classList.toggle('is-visible');
if (sibling.classList.contains('is-visible')) {
sibling.style.display = 'none'; // or set visibility to hidden
} else {
sibling.style.display = 'inline-block'; // or set visibility to visible
}
}
<button onClick={toggleComment}>i</button>
<span>{item.userComment}</span>
You can try like this:
const [backendData, setBackendData] = useState([]);
...
const showCommentsHandler = (viewComment, index) => {
let clonedBackendData = [...this.state.backendData];
clonedBackendData[index].viewComment = !viewComment;
setBackendData(clonedBackendData);
}
....
return(
<div>
....
<button onClick={() => showCommentsHandler(item.viewComment, index)}>i</button>
{item.viewComment && item.userComment}
<div>
You can store an array with that places which are clicked, for example:
const [ selectedItems, setSelectedItems] = React.useState([]);
const onClick = (el) => {
if (selectedItems.includes(el.place)) {
setSelectedItems(selectedItems.filter(e => e.place !== el.place));
} else {
setSelectedItems(selectedItems.concat(el));
}
}
and in your render function
const results = props.data.map((item, index) => {
return (
<div className='Results' key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={'read-only'}
value={item.accumRating}
style={{
width: 'auto',
alignItems: 'center',
}}
/>
<button onClick={() => onClick(item)}>i</button>
{ /* HERE */ }
{ selectedItems.includes(item.place) && <span>{item.userComment}</span> }
</div >
)
})
You need to use useState or your component won't update even if you change the property from false to true.
In order to do so you need an id since you might have more than one post.
(Actually you have a timestamp already, you can use that instead of an id.)
const [posts, setPosts] = useState([
{
id: 1,
accumRating: 3.7,
adheranceRating: 4,
cleanRating: 2,
date: "2020-10-10",
place: "PYGMALIAN",
staffRating: 5,
timestamp: { seconds: 1603315308, nanoseconds: 772000000 },
userComment: "Bad",
viewComment: false
}
]);
Create a function that updates the single property and then updates the state.
const handleClick = (id) => {
const singlePost = posts.findIndex((post) => post.id === id);
const newPosts = [...posts];
newPosts[singlePost] = {
...newPosts[singlePost],
viewComment: !newPosts[singlePost].viewComment
};
setPosts(newPosts);
};
Then you can conditionally render the comment.
return (
<div className="Results" key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={"read-only"}
value={item.accumRating}
style={{
width: "auto",
alignItems: "center"
}}
/>
<button onClick={() => handleClick(item.id)}>i</button>
{item.viewComment && <span>{item.userComment}</span>}
</div>
);
Check this codesandbox to see how it works.

How can I include my existing table into my export function?

I am relatively new to React-JS and was wondering how I could pass my variables to my export function. I am using the jsPDF library.
At the time the Summary page is showing up, every thing is already in the database.
The Summary page creates in every round an IdeaTable component, writes it into an array and renders it bit by bit if the users click on the Next button (showNextTable()).
This component can use a JoinCode & playerID to assemble the table that was initiated by this player.
import React, { Component } from "react";
import { connect } from "react-redux";
import { Box, Button } from "grommet";
import IdeaTable from "../playerView/subPages/ideaComponents/IdeaTable";
import QuestionBox from "./QuestionBox";
import { FormUpload } from 'grommet-icons';
import jsPDF from 'jspdf';
export class Summary extends Component {
state = {
shownTable: 0
};
showSummary = () => {};
showNextTable = () => {
const { players } = this.props;
const { shownTable } = this.state;
this.setState({
shownTable: (shownTable + 1) % players.length
});
};
exportPDF = () => {
var doc = new jsPDF('p', 'pt');
doc.text(20,20, " Test string ");
doc.setFont('courier');
doc.setFontType('bold');
doc.save("generated.pdf");
};
render() {
const { topic, players } = this.props;
const { shownTable } = this.state;
const tables = [];
for (let i = 0; i < players.length; i++) {
const player = players[i];
const table = (
<Box pad={{ vertical: "large", horizontal: "medium" }}>
<IdeaTable authorID={player.id} />
</Box>
);
tables.push(table);
}
return (
<Box
style={{ wordWrap: "break-word" }}
direction="column"
gap="medium"
pad="small"
overflow={{ horizontal: "auto" }}
>
<QuestionBox question={topic} />
{tables[shownTable]}
<Button
primary
hoverIndicator="true"
style={{ width: "100%" }}
onClick={this.showNextTable}
label="Next"
/>
< br />
<Button
icon={ <FormUpload color="white"/> }
primary={true}
hoverIndicator="true"
style={{
width: "30%",
background: "red",
alignSelf: "center"
}}
onClick={this.exportPDF}
label="Export PDF"
/>
</Box>
);
}
}
const mapStateToProps = state => ({
topic: state.topicReducer.topic,
players: state.topicReducer.players
});
const mapDispatchToProps = null;
export default connect(mapStateToProps, mapDispatchToProps)(Summary);
So basically how could I include the IdeaTable to work with my pdf export?
If you want to use html module of jsPDF you'll need a reference to generated DOM node.
See Refs and the DOM on how to get those.
Alternatively, if you want to construct PDF yourself, you would use data (e.g. from state or props), not the component references.
Related side note:
On each render of the parent component you are creating new instances for all possible IdeaTable in a for loop, and all are the same, and most not used. Idiomatically, this would be better:
state = {
shownPlayer: 0
};
Instead of {tables[shownTable]} you would have:
<Box pad={{ vertical: "large", horizontal: "medium" }}>
<IdeaTable authorID={shownPlayer} ref={ideaTableRef}/>
</Box>
And you get rid of the for loop.
This way, in case you use html dom, you only have one reference to DOM to store.
In case you decide to use data to generate pdf on your own, you just use this.props.players[this.state.shownPlayer]
In case you want to generate pdf for all IdeaTables, even the ones not shown, than you can't use API that needs DOM. You can still use your players props to generate your own PDF, or you can consider something like React-Pdf

How can i extract and combine inline css style properties

I read a couple of answers and all of them seems to assume you have a single object containing CSS instead of single properties, I tried the answers and I couldn't make it work, I was wondering what's wrong and what's the best way to do this, here is what I have done so far :
import React from 'react';
import FormLabel from '#material-ui/core/FormLabel';
const label = (props) => {
// <label onClick={props.onClick} className={props.className + ' ' + props.gridClass} style={props.inlineStyle} >{props.label}</label>
let divStyleArray = [];
if (typeof props.inlineStyle.background !== 'undefined') {
divStyleArray.push(props.inlineStyle.background)
delete props.inlineStyle.background
}
if (typeof props.inlineStyle.textAlign !== 'undefined') {
divStyleArray.push(props.inlineStyle.textAlign)
delete props.inlineStyle.textAlign
}
const customStyle = {
width: '100%'
}
const divStyle = Object.assign({}, ...divStyleArray);
return (
<div className={props.gridClass} style={{divStyle}}>
<FormLabel component="label" onClick={props.onClick} style={{ ...customStyle, ...props.inlineStyle }}>{props.label}</FormLabel>
</div>
)
}
export default label;
My goal is to extract a couple of CSS property, give it to the div and then give the rest to whats inside the div
Update 01:
I tried the answered given but it doesn't seem to work properly, here is what i did:
import React from 'react';
import FormLabel from '#material-ui/core/FormLabel';
const label = (props) => {
let inlineStyle = {
...props.inlineStyle
}
const divStyle = {
background: inlineStyle.background,
textAlign: inlineStyle.textAlign,
}
delete inlineStyle.background;
delete inlineStyle.textAlign;
const customStyle = {
width: '100%'
}
return (
<div className={props.gridClass} style={divStyle}>
<FormLabel component="label" onClick={props.onClick} style={{ ...customStyle, ...inlineStyle }}>{props.label}</FormLabel>
</div>
)
}
export default label;
First of all deleting stuff from the props object would be anti-pattern, or at least bad practice as far as I know.
If you only need the two properties you use there you could use this code:
const label = (props) => {
let divStyle = {
background: props.background,
textAlign: props.textAlign,
};
const customStyle = {
width: '100%'
}
return (
<div className={props.gridClass} style={{divStyle}}>
<FormLabel
component="label"
onClick={props.onClick}
style={{
...customStyle,
...props.inlineStyle,
background: undefined,
textAlign: undefined,
}}
>{props.label}</FormLabel>
</div>
)
}

Categories