I'm trying to build a component where an array of values are presented like a slider, where every time, the old content gets replaced by a new one, and that can be done using the buttons next and previous. The component is working, but I'm struggling a little bit in the edge cases, where I have to disable the buttons
I'll leave the link to a codesandbox where the component is being built, I'm sure it'll be easier to understand what's going on.
Link to sandbox
Try not to use state value for tracking button disabled status. Please check below.
import React, { useState } from "react";
export default function App() {
const data = ["q", "c", "s", "a"];
const [iterator, setIterator] = useState(0);
const [curr, setCurr] = useState(data[iterator]);
const fetchNext = () => {
if (iterator === data.length - 1) {
return;
}
setIterator((prev) => prev + 1);
setCurr(data[iterator + 1]);
};
const fetchPrevious = () => {
if (iterator === 0) {
return;
}
setIterator((prev) => prev - 1);
setCurr(data[iterator - 1]);
};
const nextDisabled = iterator >= data.length - 1;
const prevDisabled = iterator <= 0;
return (
<div>
<h1>{curr}</h1>
<button disabled={nextDisabled} onClick={fetchNext}>
next
</button>
<button disabled={prevDisabled} onClick={fetchPrevious}>
previous
</button>
</div>
);
}
Related
Problem
I am attempting to make a dynamic content box to incrementally fill in a form in ReactJS. As part of this, I simply want a box/area that changes its content when the user clicks 'Next' or 'Prev'.
The problem I appear to be facing is that the onClick method is getting called multiple times (obviously - I've only clicked it once).
I have a MWE below to demonstrate the problem I'm facing.
import React, {useEffect, useState} from 'react';
export default function Test () {
const [section, setSection] = useState(1);
const [sectionContent, setSectionContent] = useState();
useEffect( ()=>{
console.log("ENTRY useEffect");
if (section == 1) {
setSectionContent(<div>Details</div>);
}
else if(section == 2){
setSectionContent(<div>Advanced Details</div>);
}
else if(section == 3){
setSectionContent(<div>Super Advanced Details</div>);
}
console.log("EXIT useEffect");
},[section]);
function gotoPrevSection(){
console.log("ENTRY gotoPrevSection");
if(section == 1){
setSection(1);
}
else if(section == 2){
setSection(2);
}
console.log("EXIT gotoPrevSection");
}
function gotoNextSection(){
console.log("ENTRY gotoNextSection");
if (section == 1) {
setSection(2);
}else if(section == 2){
setSection(3);
}
console.log("EXIT gotoNextSection");
}
return (
<>
{sectionContent}
<div>
<button onClick={() => gotoPrevSection()}>
Prev
</button>
<button onClick={() => gotoNextSection()}>
Next
</button>
</div>
</>
);
}
Opening the page, we have 'Details' and the 'Prev' and 'Next' buttons. When I click 'Next' I skip all the way to the 'Super Advanced Details' section - skipping the middle section !
The console log looks like this:
[Log] ENTRY gotoNextSection
[Log] EXIT gotoNextSection
[Log] ENTRY useEffect
[Log] EXIT useEffect
[Log] ENTRY gotoNextSection
[Log] EXIT gotoNextSection
[Log] ENTRY useEffect
[Log] EXIT useEffect
[Log] ENTRY gotoNextSection
[Log] EXIT gotoNextSection
You can clearly see that the 'gotoNextSection' method is called three times even though it's only clicked once. What am I doing wrong ?
What I've tried
I am aware of the need for the onClick portion to look like onClick={() => function()} rather than just onClick=function(). This isn't the problem - the method isn't getting called straight away otherwise the details page wouldn't be static and stable - it's only when clicking this occurs.
I am React 17.0.2.
The Inertia init looks like:
createInertiaApp({
title: title => `${title} - ${appName}`,
resolve: name => require(`./Pages/Test.js`),
setup({ el, App, props }) {
return render(<App {...props} />, el);
},
});
What is going on here? Is this a React bug in the newer version ?
The Ghost option in Browsersync (that is included in Laravel Mix) is causing this.
Disable it to stop button double clicking ! browsersync.io/docs/options/#option-ghostMode
I've updated your functions as below, and introduce a totalSection constant.
In addition, you should be using useMemo instead of useEffect for your type of use (you save one additional render).
Codesandbox
import React, { useEffect, useState, useMemo } from "react";
const totalSections = 3;
export default function Test() {
const [section, setSection] = useState(1);
const sectionContent = useMemo(() => {
if (section == 1) {
return(<div>Details</div>);
} else if (section == 2) {
return(<div>Advanced Details</div>);
} else if (section == 3) {
return(<div>Super Advanced Details</div>);
}
},[section])
function gotoPrevSection() {
console.log("ENTRY gotoPrevSection");
setSection((prev) => (prev === 1 ? totalSections : prev - 1));
console.log("EXIT gotoPrevSection");
}
function gotoNextSection() {
console.log("ENTRY gotoNextSection");
setSection((prev) => (prev === totalSections ? 1 : prev + 1));
console.log("EXIT gotoNextSection");
}
return (
<>
{sectionContent}
<div>
<button onClick={() => gotoPrevSection()}>Prev</button>
<button onClick={() => gotoNextSection()}>Next</button>
</div>
</>
);
}
So I have a webpage that's meant to model a sort of questionnaire. Each question would take up the whole page, and the user would switch back and forth between questions using the arrow keys. That part's fine, swapping components on button pressing doesn't seem to be an issue, and I got a proof-of-concept of that working before.
The trouble is, these questionnaires won't have the same number of questions every time. That'll depend on the choice of the person making the questionnaire. So I stored the number of questions in a variable held in the Django Model, and I fetch that variable and try to generate x number of components based on that integer. At the moment I'm just trying to get it to generate the right number of components and let me navigate between them properly, I'll worry about filling them with the proper content later, because I'm already having enough trouble with just this. So here's my code:
import React, { useState, useEffect, cloneElement } from 'react';
import { useParams } from 'react-router-dom'
import QuestionTest from './questiontest';
function showNextStage(displayedTable, questionCount) {
let newStage = displayedTable + 1;
if (newStage > questionCount) {
newStage = questionCount;
}
return newStage;
}
function showPreviousStage(displayedTable) {
let newStage = displayedTable - 1;
if (newStage < 1) {
newStage = 1;
}
return newStage;
}
export default function Questionnaire(props) {
const initialState = {
questionCount: 2,
is_host: false
}
const [ roomData, setRoomData ] = useState(initialState)
const { roomCode } = useParams()
useEffect(() => {
fetch("/audio/get-room" + "?code=" + roomCode)
.then(res => res.json())
.then(data => {
setRoomData({
roomData,
questionCount: data.questionCount,
isHost: data.isHost,
})
})
},[roomCode,setRoomData])
const [ displayedTable, setDisplayedTable ] = useState(1);
let initialComponents = {};
for (let i = 1; i < roomData.questionCount + 1; i++) {
initialComponents[i] = <div><p>{i}</p> <QuestionTest /></div>
}
// "<QuestionTest />" is just a random component I made and "{i}" is
// to see if I'm on the right one as I test.
const [components] = useState(initialComponents);
useEffect(() => {
window.addEventListener('keydown', (e) => {
if (e.keyCode == '39') {
setDisplayedTable(showNextStage(displayedTable, roomData.questionCount));
} else if (e.keyCode == '37') {
setDisplayedTable(showPreviousStage(displayedTable));
}
});
return () => {
window.removeEventListener('keydown', (e) => {
if (e.keyCode == '39') {
setDisplayedTable(showNextStage(displayedTable, roomData.questionCount));
} else if (e.keyCode == '37') {
setDisplayedTable(showPreviousStage(displayedTable));
}
});
};
}, []);
return (
<div>
{components[displayedTable]}
</div>
)
}
So the trouble with this is, I need to set an initial state for the questionCount variable, or else I get errors. But this initial state is replaced almost immediately with the value set for this questionnaire, as retrieved by the fetch function, and so the state resets. The initial questionCount value of 2 however gets used in the component generation and in the adding of the eventListeners, and so when the page is generated it just has two components rather than a number matching questionCount's value for the questionnaire.
I don't really understand this tbh. If I remove the window.addEventLister from useEffect and make it standalone, then it works and the right number of components are generated, but then it adds a new EventListener every time the state refreshes, which starts to cause immense lag as you switch back and forth between pages as the function calls pile up and up.
So I don't really know how to achieve this. My entire way of going about this from the onset is probably terribly wrong (I just included it to show I made an attempt and wasn't just asking for it to be done for me), but I can't find any examples of what I'm trying to do online.
I am building an animation where the letters of two words appear one by one, similar to a slide-in effect. I have the code made with jQuery, but I need to implement it in my React app (built with hooks). The code that I have takes the text, splits it creating individual letters, and adds spans between those letters. This is the following code that I need to convert to React:
const logoText = document.querySelector('.logo');
const stringText = logoText.textContent;
const splitText = stringText.split("");
for (let i=0; i < splitText.length; i++) {
text.innerHTML += "<span>" + splitText + "</span>"
}
let char = 0;
let timer = setInterval(onTick, 50)
I was wondering if you guys could help me figure it out. Thanks a lot!
You need to iterate over the text and create a timeout function for every letter with a different time of execution, that way will be visible the slide effect you are expecting:
Custom hook
const useSlideInText = text => {
const [slide, setSlide] = useState([]);
useEffect(() => {
Array.from(text).forEach((char, index) => {
const timeout = setTimeout(
() =>
setSlide(prev => (
<>
{prev}
<span>{char}</span>
</>
)),
index * 100
);
});
}, []);
return slide;
};
Usage
function App() {
const slide = useSlideInText("hello");
return (
<div>
{slide}
</div>
);
}
Working example
I am assuming the React components that you want to run this hook in possess the text you want to split. I am also assuming that on the interval, you want to reveal more of the text. In that case my example solution would look like this:
Hook
import {useState, useEffect} from "react";
const useSlideInText = (text) => {
const [revealed, setRevealed] = useState(0);
useEffect(() => {
if (revealed < text.length) {
setTimeout(() => setRevealed(revealed + 1), 50);
}
});
return text.split('').slice(0, revealed).map((char) => (<span>{char}</span>));
}
Example usage
const MyComponent = (props) => {
const displayText = useSlideInText(props.text);
return <div>{displayText}</div>;
};
going off of the other answer:
const generateDisplayTest = (text, numChars) => text.split('').slice(0, numChars).map((char) => (<span>{char}</span>));
const MyComponent = (props) => {
const [revealed, setRevealed] = useState(0);
useEffect(() => {
if (revealed < props.text.length) {
setTimeout(() => setRevealed(revealed + 1), 50);
}
}, [revealed]);
const displayText = generateDisplayTest(props.text, revealed);
return <div>{displayText}</div>;
};
including [revealed] in the useEffect means that useEffect will run every time that revealed changes. Also I always feel that useState/useEffect should live on the component, it has been that way in the place I worked but I'm not sure if that is industry standard.
I've been learning React by doing simple games.
I'm doing a memory game which is, for the most part, finished and I'm not sure how I should refactor the code to be better.
The user task is basically to remember the numbers flashed, and then click them, until the user can't hold them in memory anymore.
How should I divide/refactor the code?
Also, how would you go forward with games like this? Would you perhaps create a gameEngine or such? Although this would be the part of refactoring right. I'm using React for games because I am learning React and improving it.
Code:
import React, { useState, useEffect } from 'react';
import Cell from './sub-components/Cell/Cell';
import './styles.scss';
function BoardScene() {
const [gameNumbers, setGameNumbers] = useState([]);
const [isPlayerTurn, setIsPlayerTurn] = useState(false);
const [currentScore, setCurrentScore] = useState(0);
const [bestScore, setBestScore] = useState(0);
const [flashCard, setFlashCard] = useState(null);
const [clickedNumber, setClickedNumber] = useState(null);
const [currentUserIndex, setCurrentUserIndex] = useState(0)
function startGame() {
addNewNumber()
}
function resetGame() {
setGameNumbers([])
setCurrentUserIndex(0)
if (bestScore < gameNumbers.length) return setBestScore(gameNumbers.length)
}
const blinkCell = () => {
let count = 0;
setIsPlayerTurn(false);
const timerID = setInterval(() => {
console.log("Inside interval before if")
console.log("currentUserIndex", count, gameNumbers.length)
if (count === gameNumbers.length) {
setIsPlayerTurn(true);
console.log("Inside time out if statement")
clearInterval(timerID);
count = 0;
setFlashCard(null);
} else {
setFlashCard(gameNumbers[count]);
count++;
}
}, 500);
};
function generateRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
function addNewNumber() {
console.log("Add new number")
let memoryNumber = generateRandomNumber(1, 9)
setGameNumbers(gameNumbers => [...gameNumbers, memoryNumber])
}
function clickedNumberHandle(number) {
console.log("Clicked number", number)
setClickedNumber(number)
isMatch(number)
}
function isMatch(number) {
if (number === gameNumbers[currentUserIndex]) {
console.log("Correct")
if (currentUserIndex + 1 === gameNumbers.length) {
setCurrentUserIndex(0)
console.log("set current index 0")
addNewNumber()
blinkCell();
} else {
console.log("set current index + 1")
setCurrentUserIndex(currentUserIndex + 1)
}
} else {
resetGame()
console.log("game over")
}
}
useEffect(() => {
blinkCell()
console.log("Use effect start blinkCell")
}, [gameNumbers])
return (
<>
<div className="game-container">
<div className="testing-stuff">
<div>
<button onClick={startGame}>Start Game</button>
</div>
<div onClick={addNewNumber}>
<button>Add new number</button>
</div>
<div>
<span>Game numbers: </span>
{gameNumbers.map((item, i) => {
return <span key={i}>{item}</span>
})}
</div>
<div>
<span>User Turn: {isPlayerTurn ? "True" : "False"}</span>
</div>
<div>
<span>Score: {gameNumbers.length}</span>
</div>
</div>
<div className="board">
{Array(9).fill().map((x, i) => {
return (
<Cell key={i} onClick={() => clickedNumberHandle(i + 1)} number={i + 1} active={i + 1 === flashCard ? true : false} />
)
})}
</div>
<div className="stats">
<div className="stats__score-wrap">
<span>Score: </span><span>{gameNumbers.length}</span>
</div>
<div className="stats__bestScore-wrap">
<span>Best Score: </span><span>{bestScore}</span>
</div>
</div>
</div>
</>
);
}
export default BoardScene;
GitHub Code: https://github.com/AurelianSpodarec/memory-numbers-game/blob/e177dfdaafe5daf393f1ae8fcee0827a16474e8f/src/scenes/BoardScene/BoardScene.js
Live Game: https://memory-numbers-game.netlify.app/
I'm no expert but I would personally re-factor to use ES6 syntax for all your functions. At the moment some of them are using arrow functions and some aren't.
I'd also personally have functions that are performing generic actions you might want to use across your codebase (such as generateRandomNumber) in their own modules.
On a specific point that isn't an opinion. I would not use the index (i) of your array as a key. This can lead to many issues. I would look into using a library like UUID to generate these keys.
Other than that looks again, and again I'm no expert. If you want some more info from someone who is I'd recommend Kent C Dodd blog and Ben Awads Youtube channel.
I'm trying to build a re-usable component to be used for pagination.
Before making you guys read more, this is how my state looks like:
I'm calling it from a component where I'm fetching post from MongoDB.
From there I'm having troubles with useEffect to make the changes and with my Pagination component that I'm not even sure if I'm building it properly.
This is my useEffect:
const params = new URLSearchParams(window.location.search);
const page = parseInt(params.get('page')) || 1;
const limit = parseInt(params.get('limit')) || 1;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
// const currentPosts = posts.slice(endIndex, startIndex);
const currentPosts = Object.entries(posts).slice(endIndex, startIndex);
console.log(currentPosts);
useEffect(() => {
getPosts(null, null, page, limit);
getCurrentProfile();
}, [getPosts, getCurrentProfile]);
// Change page
const paginate = pageNumber => page(pageNumber);
this is the component I'm calling inside the loop that is fetching the posts.
<Pagination
limit={limit}
totalPosts={posts.data.length}
paginate={paginate}
/>
Finally this is how I'm working with the component, it is obviously still a work in progress.
import React from 'react';
const setPagination = ({ limit, totalPosts, paginate }) => {
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(totalPosts / limit); i++) {
pageNumbers.push(i);
}
return (
<nav>
<ul className='pagination'>
{pageNumbers.map(number => (
<li key={number} className='page-item'>
<a
onClick={() => paginate(number)}
href={`?page=${number}&limit=${limit}`}
className='page-link'
>
{number}
</a>
</li>
))}
</ul>
</nav>
);
};
export default setPagination;
Right now, I'm trying to make it work with query strings from my url:
http://localhost:3000/posts?page=1&limit=1
Can anyone guide me on the logic that I'm missing? I'm having a headache with this xD.
EDIT:Sorry I did not make it clear. This is the problem:
Should not it create more pages?