useEffect only runs on hot reload - reactjs

I have a parent/child component where when there is a swipe event occurring in the child the parent component should fetch a new profile. The problem is the useEffect in the child component to set up the eventListeneners currently is not running, only occasionally on hot-reload which in reality should run basically every time.
Child component
function Profile(props: any) {
const [name] = useState(`${props.profile.name.title} ${props.profile.name.first} ${props.profile.name.last}`);
const [swiped, setSwiped] = useState(0)
const backgroundImage = {
backgroundImage: `url(${props.profile.picture.large})`
};
const cardRef = useRef<HTMLDivElement>(null);
const card = cardRef.current
let startX:any = null;
function unify (e:any) { return e.changedTouches ? e.changedTouches[0] : e };
function lock (e:any) { if (card) {startX = unify(e).clientX; console.log(startX)} }
function move (e: any) {
console.log('move')
if(startX) {
let differenceX = unify(e).clientX - startX, sign = Math.sign(differenceX);
if(sign < 0 || sign > 0) {
setSwiped((swiped) => swiped +1)
props.parentCallback(swiped);
startX = null
}
}
}
// Following code block does not work
useEffect(() => {
if (card) {
console.log(card)
card.addEventListener('mousedown', lock, false);
card.addEventListener('touchstart', lock, false);
card.addEventListener('mouseup', move, false);
card.addEventListener('touchend', move, false);
}
})
return (
<div>
<h1 className="heading-1">{name}</h1>
<div ref={cardRef} className="card" style={backgroundImage}>
</div>
</div>
);
}
Parent component
function Profiles() {
const [error, setError] = useState<any>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [profiles, setProfiles] = useState<any[]>([]);
const [swiped, setSwiped] = useState(0)
useEffect(() => {
getProfiles()
}, [swiped])
const callback = useCallback((swiped) => {
setSwiped(swiped);
console.log(swiped);
}, []);
const getProfiles = () => {
fetch("https://randomuser.me/api/")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setProfiles(result.results);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
}
if (error) {
return <h1 className="heading-1">Error: {error.message}</h1>;
} else if (!isLoaded) {
return <h1 className="heading-1">Loading...</h1>;
} else {
return (
<div id="board">
{profiles.map(profile => (
<Profile key={profile.id.value} profile={profile} parentCallback={callback}/>
))}
</div>
);
}
}

If you want the parent components swiped state to change, you need to pass "setSwiped" from the parent to the child compenent. You will also need to pass "swiped" to the child to use its current value to calculate the new value. I'm going to assume you declared the useState in the child component trying to set the parents state of the same name, so I'm going to remove that useState Declaration in the child altogether.
Here's an example of passing the setSwiped method and swiped value to the child:
PARENT
import React, {useState, useEffect, useCallback} from 'react';
import './Index.css';
import Profile from './Profile'
function Profiles() {
const [error, setError] = useState<any>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [profiles, setProfiles] = useState<any[]>([]);
const [swiped, setSwiped] = useState(0)
useEffect(() => {
getProfiles()
}, [swiped])
const callback = useCallback((swiped) => {
setSwiped(swiped);
console.log(swiped);
}, []);
const getProfiles = () => {
fetch("https://randomuser.me/api/")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setProfiles(result.results);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
}
if (error) {
return <h1 className="heading-1">Error: {error.message}</h1>;
} else if (!isLoaded) {
return <h1 className="heading-1">Loading...</h1>;
} else {
return (
<div id="board">
{profiles.map(profile => (
<Profile key={profile.id.value} profile={profile} parentCallback={callback} setSwiped={setSwiped} swiped={swiped}/>
))}
</div>
);
}
}
export default Profiles;
CHILD
import React, {useState, useRef, useEffect } from 'react';
import './Index.css';
function Profile(props: any) {
const [name] = useState(`${props.profile.name.title} ${props.profile.name.first} ${props.profile.name.last}`);
const backgroundImage = {
backgroundImage: `url(${props.profile.picture.large})`
};
const cardRef = useRef<HTMLDivElement>(null);
const card = cardRef.current
let startX:any = null;
function unify (e:any) { return e.changedTouches ? e.changedTouches[0] : e };
function lock (e:any) { if (card) {startX = unify(e).clientX; console.log(startX)} }
function move (e: any) {
console.log('move')
if(startX) {
let differenceX = unify(e).clientX - startX, sign = Math.sign(differenceX);
if(sign < 0 || sign > 0) {
props.setSwiped((props.swiped) => props.swiped +1)
props.parentCallback(props.swiped);
startX = null
}
}
}
useEffect(() => {
if (card) {
console.log(card)
card.addEventListener('mousedown', lock, false);
card.addEventListener('touchstart', lock, false);
card.addEventListener('mouseup', move, false);
card.addEventListener('touchend', move, false);
}
})
return (
<div>
<h1 className="heading-1">{name}</h1>
<div ref={cardRef} className="card" style={backgroundImage}>
</div>
</div>
);
}
export default Profile;
I'm hoping I didn't miss anything here.
Best of luck.

Related

TypeScript Reset countdown timer when all questions have been answered or timer hits zero

I followed a tutorial online for a react quiz using typescript. I have the countdown/timer working (with help from a friend) but now I am looking to reset it back to the original countdown value when either the last question has been answered or when the countdown timer has reached zero seconds.
App.tsx
import React, { useEffect, useState } from 'react';
// Components
import QuestionCard from './components/QuestionCard';
// Types
import { QuestionState } from './API';
import Timer, { useTimer } from './components/Timer';
import StartButton from './components/StartButton';
export type AnswerObject = {
question: string;
answer: string;
correct: boolean;
correctAnswer: string;
};
const TOTAL_QUESTIONS = 10;
const TIME_ALLOWED = 20;
const App = () => {
const [loading, setLoading] = useState(false);
const [questions, setQuestions] = useState<QuestionState[]>([]);
const [number, setNumber] = useState(0);
const [userAnswers, setUserAnswers] = useState<AnswerObject[]>([]);
const [score, setScore] = useState(0);
const [gameOver, setGameOver] = useState(true);
const { timeRemaining, toggle, reset, isActive } = useTimer(TIME_ALLOWED);
const checkAnswer = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!gameOver) {
//Users answer
const answer = e.currentTarget.value;
// Check answer against correct answer
const correct = questions[number].correct_answer === answer;
// Add score if answer is correct
if (correct) setScore((prev) => prev + 1);
// Save answer in the array for user answers
const answerObject = {
question: questions[number].question,
answer,
correct,
correctAnswer: questions[number].correct_answer,
};
setUserAnswers((prev) => [...prev, answerObject]);
console.log({ answerObject });
}
};
const nextQuestion = () => {
// Move on to the next question if not the last question
const nextQuestion = number + 1;
if (nextQuestion === TOTAL_QUESTIONS) {
setGameOver(true);
} else {
setNumber(nextQuestion);
}
};
useEffect(() => {
// If the timer reaches 0, the game is over
if (timeRemaining <= 0) {
setGameOver(true);
}
}, [timeRemaining]);
return (
<div className="App">
<h1>REACT QUIZ</h1>
{(gameOver || userAnswers.length === TOTAL_QUESTIONS) && (
<StartButton
setLoading={setLoading}
setQuestions={setQuestions}
setUserAnswers={setUserAnswers}
setNumber={setNumber}
setScore={setScore}
setGameOver={setGameOver}
toggleTimer={toggle}
/>
)}
{!gameOver ? <p className="score">Score: {score}</p> : null}
{loading && <p>Loading Questions...</p>}
{!loading && !gameOver && (
<QuestionCard
questionNr={number + 1}
totalQuestions={TOTAL_QUESTIONS}
question={questions[number].question}
answers={questions[number].answers}
userAnswer={userAnswers ? userAnswers[number] : undefined}
callback={checkAnswer}
/>
)}
{!gameOver &&
!loading &&
userAnswers.length === number + 1 &&
number !== TOTAL_QUESTIONS - 1 ? (
<button className="next" onClick={nextQuestion}>
Next Question
</button>
) : null}
<Timer
timeRemaining={timeRemaining}
isActive={isActive}
toggle={toggle}
reset={reset}
/>
</div>
);
};
export default App;
Timer.tsx
import React, { useMemo, useState, useEffect } from 'react';
export const useTimer = (timeAllowed: number) => {
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
const timeRemaining = useMemo(
() => timeAllowed - seconds,
[timeAllowed, seconds]
);
function toggle() {
setIsActive(!isActive);
}
function reset() {
setSeconds(0);
setIsActive(false);
}
useEffect(() => {
let interval: any = null;
if (isActive) {
interval = setInterval(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
} else if (!isActive && seconds !== 0) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [isActive, seconds]);
return { timeRemaining, toggle, reset, isActive };
};
const Timer = ({ timeRemaining }: any) => {
return <p className="time">{timeRemaining}s</p>;
};
export default Timer;
Startbutton.tsx
import React, { useState } from 'react';
import { Difficulty, fetchQuizQuestions } from '../API';
export type AnswerObject = {
question: string;
answer: string;
correct: boolean;
correctAnswer: string;
};
const TOTAL_QUESTIONS = 10;
interface IStartButton {
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
setQuestions: React.Dispatch<React.SetStateAction<any>>;
setUserAnswers: React.Dispatch<React.SetStateAction<any>>;
setNumber: React.Dispatch<React.SetStateAction<any>>;
setScore: React.Dispatch<React.SetStateAction<any>>;
setGameOver: React.Dispatch<React.SetStateAction<boolean>>;
toggleTimer: () => void;
}
const StartButton = ({
setLoading,
setQuestions,
setUserAnswers,
setNumber,
setScore,
setGameOver,
toggleTimer,
}: IStartButton) => {
const startTrivia = async () => {
setLoading(true);
setGameOver(false);
const newQuestions = await fetchQuizQuestions(
TOTAL_QUESTIONS,
Difficulty.EASY
);
toggleTimer();
setQuestions(newQuestions);
setScore(0);
setUserAnswers([]);
setNumber(0);
setLoading(false);
};
return (
<button className="start" onClick={startTrivia}>
Start
</button>
);
};
export default StartButton;
I thought about adding to the nextQuestion function in the if else statement that the Timer function would reset, but I wasn't too sure because of the useEffect in timer.tsx. So far when all the questions have been answered the timer keeps going until you hit the start button again. Thanks for the help in advance.

How to mock custom hook which returns scrolled element's data with jest and enzyme

In my react component I am adding a css class to a div when another div get scrolled :
import useOnScroll from '../utils';
const MyComponent = (props) => {
const scrollableContainerRef = useRef();
const { scrollTop: containerScrollTop } = useOnScroll(scrollableContainerRef);
...
return (
<div className="commonBtmSheet">
<div data-test="btmSheet" className="OverlayWrap">
<div data-test="btmSheet-header" className={`header ${containerScrollTop > 0 ? 'scrolled' : '' }`}>
...
<div data-test="btmSheet-body" ref={scrollableContainerRef} className="OverlayOuter">
...
useOnScroll Hook:
import { useEffect, useRef, useState } from "react";
function useOnScroll(ElRef) {
const prevScrollTop = useRef();
const [scrollData, setScrollData] = useState({});
const handleScroll = (e) => {
const el = e.target;
const { scrollTop } = el;
let direction = "down";
if (prevScrollTop.current > scrollTop) {
direction = "up";
}
setScrollData((prev) => ({
...prev,
scrollTop,
direction,
}));
};
useEffect(() => {
const elm = ElRef.current;
if (elm) {
elm.addEventListener("scroll", handleScroll);
}
return () => {
if (elm) {
elm.removeEventListener("scroll", handleScroll);
}
};
}, [ElRef.current]);
return scrollData;
}
export default useOnScroll;

useState and useRef don't work together properly

I'm in a case where I need to combine useState and useRef in a way, but I'm having a problem when it comes to deleting, so for example when I call the delete the update function firstly I update the state and then update the user jeff, but the deleted item firstly is deleted from the iterated and showing list, then again pop-ups, but kinda messed, the content is not the same.
Here is how I manage delete:
const Delete = (props: any, index: number, editorsRef: any): void => {
const newEmployment = cloneDeep(props.resume.employmentHistory.positions)
newEmployment.splice(index, 1)
props.setResume(
{
...props.resume, employmentHistory:
{
...props.resume.employmentHistory,
positions: newEmployment
}
}
)
const refs = {} as any;
newEmployment.map(
(position: any, index: number) =>
refs[index] = position.description
)
editorsRef.current = refs;
console.log(newEmployment, editorsRef.current)
}
And here is the iteration of the editor that has the value of useReff, and the interaction and everything related to how I change the state, something Is missing but I don't know, I really don't.
//#ts-nocheck
import { Item } from '../..';
import { WorkItemValidation } from '../../../../../utils/Validation';
import { Date, Input } from '../../../Shared';
import { Editor } from '../../..';
import { Items } from '../Components';
import { Container, Liner, Linerier } from './Components'
import { useCallback, useEffect, useRef, useState } from 'react';
import { Employment as Work } from '../../../../../utils/operations/Resume';
import { debounce } from 'lodash';
const Employment: any = (props: any) => {
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
const [loading, setLoading] = useState(true);
const editorsRef = useRef({})
const currentIndexRef = useRef('')
console.log(props, 'props', editorsRef, 'editorsRef')
useEffect(() => {
if (loading === true) {
const refs = {}
props.resume.employmentHistory.positions.map(
(position: any, index: number) =>
refs[index] = position.description
)
editorsRef.current = refs;
setLoading(false);
}
}, [])
const isFocusedRef = useRef(false);
const autoUpdateCallbackRef = useRef(null);
const callWorkDescription = useCallback(
debounce((props, currentIndex, editor) => {
Work.Description(props, currentIndex, editor);
currentIndexRef.current = ''
}, 1000),
[]
);
const handleDescription = (e: any, index: number) => {
currentIndexRef.current = index
editorsRef.current = {
...editorsRef.current,
[index]: e,
}
callWorkDescription(props, index, e)
};
useEffect(() => {
const intervalId = setInterval(() => {
if (isFocusedRef.current) autoUpdateCallbackRef.current?.();
}, 1000);
return () => clearInterval(intervalId);
}, []);
const handleFocus = () => isFocusedRef.current = true;
const handleBlur = () => isFocusedRef.current = false;
return (
<Container>
{
loading === false &&
<Items>
{
props.resume.employmentHistory.positions.map(
(position: any, index: number) => {
return (
<Item delete={() => Work.Delete(props, index, editorsRef)}
>
<Liner style={{ width: '100%' }}>
<Editor
id={`editor-${index}`}
label="Description"
value={editorsRef.current[index] && editorsRef.current[index] || ''}
placeholder="Brief description of the role..."
onChange={(e) => handleDescription(e, index)}
onFocus={handleFocus}
onBlur={handleBlur}
/>
</Liner>
</Item>
)
}
)
}
</Items>
}
</Container >
)
}
export default Employment;

Not Rendering Card - React

I'm new to React, and I would like to know if someone can help me?
I'm trying to use useEffect and State to manipulate the API.
But the cards are not rendering.
Sometimes all the cards are rendering, other times not.. and they always come on a different order even after sorting them :( Can you help me?
App.js
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect } from "react";
import PlayerList from "./PlayerList";
import axios from "axios";
function App() {
const Team = [
...
];
const Team2 = [
...
];
const Team3 = [
...
];
const teamForLoop = [Team, Team2, Team3];
const [allPlayers, setAllPlayers] = useState([]);
const [team, setTeam] = useState([]);
const [allTeams] = useState(teamForLoop);
const [loading, setLoading] = useState(true);
useEffect(() => {
const playerInfo = async () => {
setLoading(true);
allTeams.map(async (teamArray) => {
setTeam([]);
teamArray.map(async (player) => {
let playerName = player.split(" ");
const result = await axios.get(
`https://www.thesportsdb.com/api/v1/json/2/searchplayers.php?p=${playerName[0]}%20${playerName[1]}`
);
if (result.data.player === null) {
setTeam((state) => {
return [...state];
});
} else {
setTeam((state) => {
return [...state, result.data.player[0]];
});
}
});
setAllPlayers(team);
});
setLoading(false);
};
playerInfo();
}, [allTeams]);
if (loading) return "...Loading...";
return (
<>
<PlayerList allPlayers={allPlayers} />
</>
);
}
export default App;
PlayerList.js
import React from "react";
export default function PlayerList({ allPlayers }) {
const myData = []
.concat(allPlayers)
.sort((a, b) => (a.strNumber > b.strNumber ? 1 : -1))
.sort((a, b) => (a.idTeam !== b.idTeam ? 1 : -1));
return (
<div>
{myData.map((player, index) => (
<div key={index}>
<div className="playerCard">
<img
className="playerImage"
src={player.strCutout}
alt={`${player.strPlayer}`}
/>
<h1 className="playerName">{player.strPlayer}</h1>
<h2 className="playerNumber">{player.strNumber}</h2>
</div>
</div>
))}
</div>
);
}
Codesandbox link:
"https://codesandbox.io/s/busy-orla-v872kt?file=/src/App.js"

Unable to update react state with an array

I can make a successful call to getApiSuggestions with data returned. However I'm unable to assign this to my state.
As you can see my console output shows that the value for response has an array. However, when attempting to assign it to wikiResults:response the array remains empty.
note that this is a modification of react-search-autocomplete
Am I attempting to pass the variables incorrectly?
NarrativeSearch.js
import React, {useContext, useState, useEffect} from "react";
import './search.css'
import { ReactSearchAutocomplete } from 'react-search-autocomplete'
import { getApiSuggestions } from '../../requests/requests';
import {TextSearchContext} from "../../contexts/TextSearchContext"
import {SearchContext} from "../../contexts/SearchContext"
function Search() {
const {textFilterState, setTextFilterState} = useContext(TextSearchContext);
const [wikiTitleResults, setWikiTitleResults] = useState({wikiResults:[]});
var cnJson = wikiTitleResults;
const items = wikiTitleResults.wikiResults;
const handleOnSearch = (string, results) => {
console.log("STRING: ", string)
getApiSuggestions(string).then(response => {
console.log("RESPONSE: ", response);
setWikiTitleResults({wikiResults:response}); //<---- This doesn't update the state
console.log("WikiTitle: ", wikiTitleResults.wikiResults);
console.log("Items: ", items);
})
}
const handleOnHover = (result) => {
// the item hovered
console.log(result)
}
const handleOnSelect = (item) => {
// the item selected
setTextFilterState({textFilter:item.name});
console.log(item)
}
const handleOnFocus = () => {
console.log('Focused')
}
const handleOnClear = () => {
setTextFilterState({textFilter:""});
}
const formatResult = (item) => {
return (
<>
<span style={{ display: 'block', textAlign: 'left' }}>id: {item.title}</span>
</>
)
}
return (
<div >
<div className="searchbar">
<ReactSearchAutocomplete
items={items}
onSearch={handleOnSearch}
onHover={handleOnHover}
onSelect={handleOnSelect}
onFocus={handleOnFocus}
onClear={handleOnClear}
styling={{ zIndex: 4 }} // To display it on top of the search box below
autoFocus
/>
</div>
</div>
)
}
export default Search
getApiSuggesetions
const getApiSuggestions = (title) => {
//console.log("URL Being called"+ urlSingleResult);
//console.log(title);
let result = urlMultiResult
.get(`${title}`)
.then((response) => {
console.log(Object.values(response.data.query.pages))
return Object.values(response.data.query.pages);
})
.catch((error) => {
return error;
console.log(error);
});
console.log(result);
return result;
};
I fixed this by including a useEffect and a context from the parent component.
function Search() {
const {textFilterState, setTextFilterState} = useContext(TextSearchContext);
const {wikiTitleResults, setWikiTitleResults} = useContext(SearchContext);
var items = wikiTitleResults.wikiTitles;
useEffect(() => {
const fetchData = async () => {
const data = await getApiSuggestions(textFilterState.textFilter)
setWikiTitleResults({wikiTitles:data})
}
fetchData();
},
[textFilterState])
const handleOnSearch = (string, results) => {
setTextFilterState({textFilter:string});
}

Resources