How to test an if sentence inside useEffect? - reactjs

I'm trying to test an if sentence inside a useEffect which main dependency, riskSelection, comes from a useSelector from react-redux library. Jest coverage indicates line 20-21 is missing.
App.tsx
function App(): JSX.Element {
const riskSelection = useAppSelector(selectRiskSelection);
const {save} = useActions({...riskSelectorActions});
const risks: Risk = risks_levels;
const [data, setData] = useState<Data[]>();
const width = 450,
height = 450,
margin = 40;
const radius = Math.min(width, height) / 2 - margin;
useEffect(() => {
if (typeof riskSelection !== 'undefined') { // line 20
setData(Object.values(risks[riskSelection])); // line 21
}
}, [riskSelection, risks]);
return (
<div>
<ul className={style.riskSelectorUl}>
{new Array(10).fill(0).map((v, i: number) => (
<li
className={Number(riskSelection) === i + 1 ? style.selected : ''}
onClick={() => save({riskSelection: `${i + 1}`})}
key={i}
>
{i + 1}
</li>
))}
</ul>
{data && (
<PieSVG
data={data}
width={width}
height={height}
innerRadius={100}
outerRadius={radius}
/>
)}
</div>
);
}
export default App;
My test should verify if PieSVG has been rendered or not. In order to do so I have to simulate a change in riskSelection. This is where I need help.
App.test.tsx
jest.mock('../utils/redux/hooks');
import * as reduxHooks from '../utils/redux/hooks';
describe('App', () => {
let wrapper: any;
beforeEach(() => {
jest
.spyOn(reduxHooks, 'useAppSelector')
.mockImplementation((f) => f({riskSelector: {riskSelection: undefined}}));
wrapper = shallow(<Dummy />);
});
afterEach(() => {
jest.clearAllMocks();
});
it('shows pie chart', () => {
reduxHooks.useAppSelector(() => '1');
expect(wrapper.find(PieSVG)).toHaveLength(1);
});
});
I use mockImplementation to try to change riskSelector but that just initializes my redux state.
The user trigger this change of redux state when it click on a <li /> which uses save action.
Maybe I should not make my UI changes so dependent on a useEffect?
hook.ts
import {TypedUseSelectorHook, useSelector} from 'react-redux';
import type {RootState} from './store';
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Related

why useRef current value , isn't sharing trough custom hook?

I wanted to calculate the user scroll height , so I created a custom hook. and I wanted to share this value to another component. but it doesnt work.
code:
const useScroll = () => {
let scrollHeight = useRef(0);
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", () => {});
};
}, []);
return scrollHeight.current;
};
export default useScroll;
the value is not updating here.
but if I use useState here , it works. but that causes tremendous amount of component re-rendering. can you have any idea , how its happening?
Since the hook won't rerender you will only get the return value once. What you can do, is to create a useRef-const in the useScroll hook. The useScroll hook returns the reference of the useRef-const when the hook gets mounted. Because it's a reference you can write the changes in the useScroll hook to the useRef-const and read it's newest value in a component which implemented the hook. To reduce multiple event listeners you should implement the hook once in the parent component and pass the useRef-const reference to the child components. I made an example for you.
The hook:
import { useCallback, useEffect, useRef } from "react";
export const useScroll = () => {
const userScrollHeight = useRef(0);
const scroll = useCallback(() => {
userScrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
}, []);
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", scroll);
};
}, []);
return userScrollHeight;
};
The parent component:
import { SomeChild, SomeOtherChild } from "./SomeChildren";
import { useScroll } from "./ScrollHook";
const App = () => {
const userScrollHeight = useScroll();
return (
<div>
<SomeChild userScrollHeight={userScrollHeight} />
<SomeOtherChild userScrollHeight={userScrollHeight} />
</div>
);
};
export default App;
The child components:
export const SomeChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "aqua"
}}>
<h1>SomeChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
export const SomeOtherChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeOtherChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "orange"
}}>
<h1>SomeOtherChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
import { useRef } from 'react';
import throttle from 'lodash.throttle';
/**
* Hook to return the throttled function
* #param fn function to throttl
* #param delay throttl delay
*/
const useThrottle = (fn, delay = 500) => {
// https://stackoverflow.com/a/64856090/11667949
const throttledFn = useRef(throttle(fn, delay)).current;
return throttledFn;
};
export default useThrottle;
then, in your custom hook:
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
const throttledScroll = useThrottle(scroll)
Also, I like to point out that you are not clearing your effect. You should be:
useEffect(() => {
window.addEventListener("scroll", throttledScroll);
return () => {
window.removeEventListener("scroll", throttledScroll); // remove Listener
};
}, [throttledScroll]); // this will never change, but it is good to add it here. (We've also cleaned up effect)

React state doesnt update, drilling seems ok though

I've got a modal that I want to be able to auto-shut itself using a drilled function. The console.log does work, but the state isn't actually updating. What am I doing wrong? Triggering the state via the dev tools works fine, so it's not the state itself. Is drilling within a component the problem?
index.js:
export default function Home() {
const [modalOpen, setModalOpen] = useState(false)
const handleModalOpen = () => {
console.log ("Setting modal to true")
setModalOpen (true)
}
const handleModalClose = () => {
console.log ("Setting modal to false")
setModalOpen (false)
}
// all the normal app body
{modalOpen ?
(<Modal handleModalClose={handleModalClose} height='30vh'>
<h4>Thank you for your contact request.</h4>
<h4>Keep in mind that this is only a demo website, not an actual business.</h4>
</Modal>): null}
</div>
)
}
Modal.js:
import { createPortal } from "react-dom";
import { useEffect, useState } from "react";
import styles from '../styles/Modal.module.css'
const Backdrop = (props) => {
return <div onClick={() => props.handleModalClose()} className={styles.backdrop} />
}
const Message = (props) => {
let width = '70vw'
let height = '80vh'
if (props.width) width = props.width
if (props.height) height = props.height
return (
<div style={{ width: width, height: height }} className={styles.message}>
{props.children}
</div>
)
}
const Modal = (props) => {
const [backdropDiv, setBackdropDiv] = useState(null)
const [modalDiv, setModalDiv] = useState(null)
useEffect(() => {
if (typeof (window) !== undefined) {
let backdropDiv = document.getElementById('backdrop')
setBackdropDiv(backdropDiv)
let modalDiv = document.getElementById('modal')
setModalDiv(modalDiv)
}
}, [])
return (
<>
{backdropDiv !== null && modalDiv !== null ? (
<>
{createPortal(<Backdrop handleModalClose = {props.handleModalClose} />, backdropDiv)}
{createPortal(<Message children={props.children} width={props.width} height={props.height} />, modalDiv)}
</>
) : null
}
</>
)
}
export default Modal

useEffect fails on page refresh

I am an infant programmer and I am trying to fetch an api and style the results using React. My page works fine on the initial load and subsequent saves on VScode,but when I actually refresh the page from the browser I get the error thats posted on imageenter image description here:
Here is my code: App.js
```import React, { useEffect, useState } from 'react';
import './App.css';
import Students from './components/Students';
import styled from 'styled-components';
function App() {
const [studentInfo, setStudentInfo] = useState({});
const [searchResult, setSearchResult] = useState({});
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
getStudents();
}, []);
useEffect(() => {
getStudents();
console.log('useEffect');
}, [searchTerm]);
const getStudents = async () => {
const url = 'https://api.hatchways.io/assessment/students';
console.log(url);
fetch(url)
.then((res) => res.json())
.then((data) => {
console.log(data);
searchTerm != ''
? setStudentInfo(filterStudents(data.students))
: setStudentInfo(data.students);
});
};
const filterStudents = (studentsArray) => {
return studentsArray.filter((info) => {
return (
info.firstName.toLowerCase().includes(searchTerm) ||
info.lastName.toLowerCase().includes(searchTerm)
);
});
};
console.log(searchTerm);
return (
<div className="App">
<Students
studentInfo={studentInfo}
setSearchTerm={setSearchTerm}
searchTerm={searchTerm}
/>
</div>
);
}
export default App;```
here is my component Students.js:
```import React, { useState } from 'react';
import styled from 'styled-components';
import GradeDetails from './GradeDetails';
const Students = ({ studentInfo, searchTerm, setSearchTerm }) => {
console.log(typeof studentInfo);
console.log(studentInfo[0]);
const [isCollapsed, setIsCollapsed] = useState(false);
const handleDetails = () => {
setIsCollapsed(!isCollapsed);
};
const average = (arr) => {
let sum = 0;
arr.map((num) => {
sum = sum + parseInt(num);
});
return sum / arr.length.toFixed(3);
};
console.log(isCollapsed);
return (
<Container>
<Input
type="text"
value={searchTerm}
placeholder="Search by name"
onChange={(e) => setSearchTerm(e.target.value.toLowerCase())}
/>
{studentInfo?.map((student) => (
<Wrapper key={student.id}>
<ImageContainer>
<Image src={student.pic}></Image>
</ImageContainer>
<ContentContainer>
<Name>
{student.firstName} {student.lastName}{' '}
</Name>
<Email>Email: {student.email}</Email>
<Company>Company: {student.company}</Company>
<Skills>Skill: {student.skill}</Skills>
<Average>Average:{average(student.grades)}%</Average>
</ContentContainer>
<ButtonContainer>
<Button onClick={handleDetails}>+</Button>
</ButtonContainer>
{isCollapsed && <GradeDetails studentInfo={studentInfo} />}
</Wrapper>
))}
</Container>
);
};```
Every time I have the error, I comment out the codes in Students.js starting from studentInfo.map until the and save and then uncomment it and save and everything works fine again.
I am hoping someone can help me make this work every time so that I don't have to sit at the edge of my seat all the time. Thank you and I apologize for the long question.
You are using an empty object as the initial state for studentInfo (the value passed to useState hook will be used as the default value - docs):
const [studentInfo, setStudentInfo] = useState({});
.map is only supported on Arrays. So this is failing when the component is rendering before the useEffect has completed and updated the value of studentInfo from an object, to an array. Try swapping your initial state to be an array instead:
const [studentInfo, setStudentInfo] = useState([]);

Call a function from a class in a different file - React

I'm basically trying to call a function (getValue) from a class (Time) in a different file, but there is some issues.
Here is the code for the two files:
Time.js
export default class Time extends Component {
constructor(props) {
super(props);
this.state = {
input: '',
input2: '',
checked: false
}
this.getValue = this.getValue.bind(this);
}
hrChange = e => {
this.setState({input: e.target.value}, function () {this.getValue()})
}
minChange = e => {
this.setState({input2: e.target.value}, function () {this.getValue()})
}
amPm = () => {
this.setState({checked: !this.state.checked}, function () {this.getValue()})
}
getValue = () => {
const list = [
this.state.input,
this.state.input2,
this.state.checked
]
return (list)
}
render() {
return(
<text>some stuff</text>
)
}
}
NewStorage.js
function NewStorage() {
const time = () => {
var obj = new Time();
var list = obj.getValue()
const
hrInput = list[0],
minInput = list[1],
pm = list[2]
return(
console.log(hrInput, minInput, pm, list)
)
return(
time()
)
}
export default NewLocalStorage;
The main issue isn't that I can't call the function, it is that when I call the function, the values of input, input2, and checked are all the original value ('', '', false), not the updated versions (ex: '11', '30', true).
I'm not sure on how to solve this issue.
Your inclusion of the react-hooks tag suggest your hunch that hooks are applicable to solving your problem. I would agree -
const { useState, useEffect } = React
function Time ({ hour, minute, onChange }) {
const [h,setHour] = useState(hour)
const [m,setMinute] = useState(minute)
useEffect(_ => onChange({ hour: h, minute: m }), [h, m])
return <div>
<input value={h} onChange={event => setHour(event.target.value)} />
<input value={m} onChange={event => setMinute(event.target.value)} />
</div>
}
ReactDOM.render(<Time onChange={console.log} />, document.querySelector("main"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<main></main>
In a more sophisticated example, we can use the Time component's onChange callback to update nested state in a parent component, MyForm -
const { useState, useEffect, useCallback } = React
function Time ({ hour = 0, minute = 0, onChange }) {
const [h,setHour] = useState(hour)
const [m,setMinute] = useState(minute)
useEffect(_ => onChange({ hour: h, minute: m }), [h, m, onChange])
return <div>
<input value={h} onChange={event => setHour(event.target.value)} />
<input value={m} onChange={event => setMinute(event.target.value)} />
</div>
}
function MyForm () {
const [data, setData] = useState({ time: { hour: 5, minute: 30 }, foo: "bar" })
const onTimeChange = useCallback(t => setData({ ...data, time: t }), [])
return <form>
<Time hour={data.time.hour} minute={data.time.minute} onChange={onTimeChange} />
<pre>{JSON.stringify(data, null, 2)}</pre>
</form>
}
ReactDOM.render(<MyForm />, document.querySelector("main"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<main></main>
Instead of trying to create a class and call the function in another file, why not use React functional components and hooks?
Try something like this:
const Clock = () => {
const [hour, setHour] = useState();
const [min, setMin] = useState();
const [am, setAm] = useState(true);
useEffect(() => {
// Get your clock to work in here...
}, [hour, min, am]);
return (
<div>
{//This will post your clock here, and if you need the values, you
can set/use them individually as needed.}
{hour}:{min} {am ? 'am' : 'pm'}
{//The ternary statement will modify this portion for you in code.}
</div>
);
}
If you want to use the values globally, you may want to try using the React hook useContext(). This will allow you to access those specific values anywhere you want, but requires a bit more setup.
Context, if you don't know will turn your react app into Redux, without using Redux. Below is an example of what you need to do.
import { createContext } from "react";
export const QuizContext = createContext();
then you add the context to your App.js:
import { useState } from 'react';
import './App.css';
import MainMenu from './Components/MainMenu';
import Quiz from './Components/Quiz';
import EndScreen from './Components/EndScreen';
import { QuizContext } from './Helpers/Context';
function App() {
const [gameState, setGameState] = useState('Menu');
const [score, setScore] = useState(0);
return (
<div className="App">
<h1>Quiz App</h1>
<QuizContext.Provider value={{gameState, setGameState, score, setScore}}>
{gameState === 'Menu' && <MainMenu/>}
{gameState === 'Quiz' && <Quiz/>}
{gameState === 'EndScreen' && <EndScreen/>}
</QuizContext.Provider>
</div>
);
}
Then you can access the context from individual components as long as they are children of App.
Example:
import React, { useContext, useState } from 'react';
import { QuizContext } from '../Helpers/Context';
import {Questions} from '../Helpers/QuestionBank'
const Quiz = () => {
const [currentQuestion, setCurrentQuestion] = useState(0)
const [optionChosen, setOptionChosen] = useState('');
const {setGameState, score, setScore} = useContext(QuizContext);
const nextQuestion = () => {
Questions[currentQuestion].answer === optionChosen ? setScore(score + 1) : console.log(score);
setCurrentQuestion(currentQuestion + 1);
}
const finishQuiz = () => {
Questions[currentQuestion].answer === optionChosen ? setScore(score + 1) : console.log(score);
setGameState('EndScreen');
}
return (
<div className="Quiz">
<h1>{Questions[currentQuestion].prompt}</h1>
<div className="options">
<button onClick={() => setOptionChosen('optionA')}>{Questions[currentQuestion].optionA}</button>
<button onClick={() => setOptionChosen('optionB')}>{Questions[currentQuestion].optionB}</button>
<button onClick={() => setOptionChosen('optionC')}>{Questions[currentQuestion].optionC}</button>
<button onClick={() => setOptionChosen('optionD')}>{Questions[currentQuestion].optionD}</button>
</div>
{currentQuestion === Questions.length -1 ? <button onClick={finishQuiz}>Finish Quiz</button> : <button onClick={nextQuestion}>Next Question</button>}
</div>
)
}
export default Quiz
I learned this method from a Tutorial from PedroTech on YouTube. I followed along to create this. I wanted to make sure I didn't take credit for his work.

Testing slider component in react

I want to test my slider component with react testing library. But I can't comprehend how to test it properly. I want to test changing slide when the user clicks the dot(StyledDotContainer). StyledDotContainer's background is gray but it is red when the active props is true. The component looks like this.
const Slider = ({
children,
autoPlayTime = 5000,
dots = true,
initialIndex= 0
}: SliderProps): React.ReactElement => {
const [activeIndex, setActiveIndex] = useState<number>(initialIndex)
const nextSlide = () => {
const newIndex = activeIndex >= length - 1 ? 0 : activeIndex + 1
setActiveIndex(newIndex)
}
useEffect(() => {
const timer = setTimeout(() => {
nextSlide()
}, autoPlayTime)
return () => clearTimeout(timer)
}, [activeIndex])
const length = useMemo(() => {
return React.Children.count(children)
}, [])
const setSlide = useCallback((index: number) => {
setActiveIndex(index)
}, [])
const value = useMemo(() => ({ activeIndex, setSlide }), [activeIndex])
return (
<SliderContext.Provider value={value}>
<StyledContainer>
{children}
{dots && (
<StyledDotsContainer data-testid="dots">
{[...Array(length)].map((_, index) => {
return (
<StyledDotContainer
data-testid={`dot-${index}`}
key={index}
onClick={() => setSlide(index)}
isActive={index === activeIndex}
/>
)
})}
</StyledDotsContainer>
)}
</StyledContainer>
</SliderContext.Provider>
)
}
Appreciate any suggestion.
It's a bad practice to test the styles of a Component. Instead you just want to test that the function you are trying to test is properly changing the props in your component. Styles should be inspected visually.
import screen from '#testing-library/dom'
import {render, screen} from '#testing-library/react'
import userEvent from '#testing-library/user-event'
describe("Slider", () => {
it('Should be red when slider is active', () => {
render(Slider)
const firstDot = screen.getByTestId('dots-0')
act(() => {
userEvent.click(firstDot)
})
waitFor(() => {
expect(screen.getByTestId('dots-0').props.isActive).toBeTruthy()
})
})
})

Resources