useState wouldn't work asynchronously (React) - reactjs

Building a recording app on React + Type Script. I tried to set state with getting stream, and it seems to be successfully gotten on console. But it couldn't be set up on record target stream.
const [recordTargetStream, setRecordTargetStream] = useState<MediaStream>()
// click request permissions
const requestPermissions = useCallback(async() => {
const stream = await window.navigator.mediaDevices.getUserMedia({audio: true, video: true})
// stream is successfully gotten
setRecordTargetStream(stream)
}, [])
const startRecording = useCallback(() => {
console.log('start recording', recordTargetStream)
// record target stream is undefined
...
return (
<>
<button onClick={() => requestPermissions()}>Request permissions</button>
<button onClick={() => startRecording()}>Start recording</button>
</>
)

You should use the recordTargetStream state as useCallback hook's dependencies.
useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed.
every value referenced inside the callback should also appear in the dependencies array.
index.tsx:
import React, { useCallback, useState } from 'react'
export default function MyComponent() {
const [recordTargetStream, setRecordTargetStream] = useState<MediaStream>()
const requestPermissions = useCallback(async() => {
const stream = await window.navigator.mediaDevices.getUserMedia({audio: true, video: true})
setRecordTargetStream(stream)
}, [])
const startRecording = useCallback(() => {
console.log('start recording', recordTargetStream)
}, [recordTargetStream])
return (
<>
<button onClick={() => requestPermissions()}>Request permissions</button>
<button onClick={() => startRecording()}>Start recording</button>
</>
)
}
index.test.tsx:
import {fireEvent, render, screen, act} from '#testing-library/react';
import React from 'react';
import MyComponent from './';
describe('69354798', () => {
test('should pass', async () => {
const mockMediaDevices = {
getUserMedia: jest.fn().mockResolvedValue('test stream')
}
Object.defineProperty(window.navigator, 'mediaDevices', {
value: mockMediaDevices
})
render(<MyComponent/>)
await act(async () => {
fireEvent.click(screen.getByText(/Request permissions/))
})
fireEvent.click(screen.getByText(/Start recording/))
})
})
test result:
PASS stackoverflow/69354798/index.test.tsx
69354798
✓ should pass (54 ms)
console.log
start recording test stream
at stackoverflow/69354798/index.tsx:12:13
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.211 s

Related

React/Jest function component testing

How can I write a test case for my function component which is having API call to return data on load? For example consider below code:
//List of tasKs and process tasK information
const TaskList = props => {
/**Function to load data on component initialization */
useEffect(() => {
getData(true);
}, []);
/**
* Function to pull tasks from database
* #param {filters} applyfilter
*/
const getData = async applyfilter => {
try {
let taskData = await service.getTasks()
taskData.tasks = taskData.tasks.sort(GFG_sortFunction)
let status_data = taskData.tasks.filter(values => {
if (statuses.includes(values.status_id)) {
return true
}
})
setData(status_data);
setT_Data(taskData.tasks);
setshowGenerateDPF(true);
builtFilter(taskData.tasks);
if ((selectedPlatforms.length !== 0 || selectedAssets.length !== 0 || selectedCategories.length !== 0 || selectedStatus.length !== 0) && applyfilter)
FilterTasksData(selectedPlatforms, selectedAssets, selectedCategories, selectedStatus, taskData.tasks);
}
catch (err) {
throw err;
}
};
return (
<>
<Row>
<div data-testid="tasklist" style={{ float: "left", padding: "0" }} className={[classes.taskList, "col-md-8"].join(' ')}>
<span style={{ float: "left" }}>
{tasks.length} Tasks </span>
</div>
</Row>
</>
}
I want to write unit test case for getData method. How can I do that?
You would want to mock the response and the other functions (like setData for example)
Here is a random example of mocking a fetch request with "react testing library" that renders div with test id that outputs text data from request
import YourComponent from '../'
import React from 'react';
import '#testing-library/jest-dom/extend-expect';
import { render, waitForElement } from '#testing-library/react';
describe('59892259', () => {
let originFetch;
beforeEach(() => {
originFetch = (global as any).fetch;
});
afterEach(() => {
(global as any).fetch = originFetch;
});
it('should pass', async () => {
const fakeResponse = { data: 'example data' };
const mRes = { json: jest.fn().mockResolvedValueOnce(fakeResponse) };
const mockedFetch = jest.fn().mockResolvedValueOnce(mRes as any);
(global as any).fetch = mockedFetch;
const { getByTestId } = render(<YourComponent />);
const div = await waitForElement(() => getByTestId('test'));
expect(div).toHaveTextContent('example data');
expect(mockedFetch).toBeCalledTimes(1);
expect(mRes.json).toBeCalledTimes(1);
});
});
you can write up a test that will expect to see "{tasks.length} Tasks" span's text as anything you mock, just make sure to mock or import the global store(redux/mobx) or hooks in your testing env so you will be able to mock the method calls as well

How to correctly use Hooks in React?

I am new to React, and I have to build a timeout mechanism for a page. I used react-idle-timer, with some help found on the Internet. However, when I try to access the page, I get a Minified React error #321, in which it tells me that I used hooks incorrectly.
Can you please take a look on the following code and point me in the right direction? Thanks
import React from "react"
import NavBar from "./Navbar"
import "../styles/Upload.css"
import LinearProgressWithLabel from "./LinearProgressWithLabel"
import axios from "axios"
import Logout from "./Logout"
import { useIdleTimer } from 'react-idle-timer'
import { format } from 'date-fns'
export default function Upload() {
const [selectedFile, setSelectedFile] = React.useState();
const [progress, setProgress] = React.useState(0);
const timeout = 3000;
const [remaining, setRemaining] = React.useState(timeout);
const [elapsed, setElapsed] = React.useState(0);
const [lastActive, setLastActive] = React.useState(+new Date());
const [isIdle, setIsIdle] = React.useState(false);
const handleOnActive = () => setIsIdle(false);
const handleOnIdle = () => setIsIdle(true);
const {
reset,
pause,
resume,
getRemainingTime,
getLastActiveTime,
getElapsedTime
} = useIdleTimer({
timeout,
onActive: handleOnActive,
onIdle: handleOnIdle
});
const handleReset = () => reset();
const handlePause = () => pause();
const handleResume = () => resume();
React.useEffect(() => {
setRemaining(getRemainingTime())
setLastActive(getLastActiveTime())
setElapsed(getElapsedTime())
setInterval(() => {
setRemaining(getRemainingTime())
setLastActive(getLastActiveTime())
setElapsed(getElapsedTime())
}, 1000)
}, []);
function changeHandler(event) {
setSelectedFile(event.target.files[0])
};
function handleSubmission() {
if (selectedFile) {
var reader = new FileReader()
reader.readAsArrayBuffer(selectedFile);
reader.onload = () => {
sendFileData(selectedFile.name, new Uint8Array(reader.result), 4096)
};
}
};
function sendFileData(name, data, chunkSize) {
function sendChunk(offset) {
var chunk = data.subarray(offset, offset + chunkSize) || ''
var opts = { method: 'POST', body: chunk }
var url = '/api/uploaddb?offset=' + offset + '&name=' + encodeURIComponent(name)
setProgress(offset / data.length * 100)
fetch(url, opts).then(() => {
if (chunk.length > 0) {
sendChunk(offset + chunk.length)
}
else {
axios.post('/api/uploaddb/done', { name })
.then(setProgress(100))
.catch(e => console.log(e));
}
})
}
sendChunk(0);
};
return (
<div>
<NavBar />
<div>
<div>
<h1>Timeout: {timeout}ms</h1>
<h1>Time Remaining: {remaining}</h1>
<h1>Time Elapsed: {elapsed}</h1>
<h1>Last Active: {format(lastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
<h1>Idle: {isIdle.toString()}</h1>
</div>
<div>
<button onClick={handleReset}>RESET</button>
<button onClick={handlePause}>PAUSE</button>
<button onClick={handleResume}>RESUME</button>
</div>
</div>
<h1>Upload</h1>
<input type="file" name="file" onChange={changeHandler} />
{!selectedFile ? <p className="upload--progressBar">Select a file</p> : <LinearProgressWithLabel className="upload--progressBar" variant="determinate" value={progress} />}
<br />
<div>
<button disabled={!selectedFile} onClick={handleSubmission}>Submit</button>
</div>
</div>
)
}
Well, in this case, you should avoid setting states inside the useEffect function, because this causes an infinite loop. Everytime you set a state value, your component is meant to render again, so if you put states setters inside a useEffect function it will cause an infinite loop, because useEffect function executes once before rendering component.
As an alternative you can set your states values outside your useEffect and then put your states inside the useEffect array param. The states inside this array will be "listened" by useEffect, when these states change, useEffect triggers.
Something like this:
React.useEffect(() => {
}, [state1, state2, state3]);
state anti-pattern
You are using a state anti-pattern. Read about Single Source Of Truth in the React Docs.
react-idle-timer provides getRemainingTime, getLastActiveTime and getElapsedTime
They should not be copied to the state of your component
They are not functions
getRemainingTime(), getLastActiveTime(), or getElapsedTime() are incorrect
To fix each:
getRemainingTime should not be stored in state of its own
Remove const [remaining, setRemaining] = useState(timeout)
Remove setRemaining(getRemainingTime) both places in useEffect
Change <h1>Time Remaining: {remaining}</h1>
To <h1>Time Remaining: {getRemainingTime}</h1>
The same is true for lastActive.
getLastActive should be be stored in state of its own
Remove const [lastActive, setLastActive] = React.useState(+new Date())
Remove setLastActive(getLastActiveTime()) both places in useEffect
Change <h1>Last Active: {format(lastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
To <h1>Last Active: {format(getLastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
And the same is true for elapsed.
getElapsedTime should be be stored in state of its own
Remove const [elapsed, setElapsed] = React.useState(+new Date())
Remove setElapsed(getElapsedTime()) both places in useEffect
Change <h1>Time Elapsed: {elapsed}</h1>
To <h1>Time Elapsed: {getElapsedTime}</h1>
remove useEffect
Now your useEffect is empty and it can be removed entirely.
unnecessary function wrappers
useIdleTimer provides reset, pause, and resume. You do not need to redefine what is already defined. This is similar to the anti-pattern above.
Remove const handleReset = () => reset()
Change <button onClick={handleReset}>RESET</button>
To <button onClick={reset}>RESET</button>
Remove const handlePause = () => pause()
Change <button onClick={handlePause}>PAUSE</button>
To <button onClick={pause}>PAUSE</button>
Remove const handleResume = () => resume()
Change <button onClick={handleResume}>RESUME</button>
To <button onClick={resume}>RESUME</button>
avoid local state
timeout should be declared as a prop of the Upload component
Remove const timeout = 3000
Change function Upload() ...
To function Upload({ timeout = 3000 }) ...
To change timeout, you can pass a prop to the component
<Upload timeout={5000} />
<Upload timeout={10000} />
use the provided example
Read Hook Usage in the react-idle-timer docs. Start there and work your way up.
import React from 'react'
import { useIdleTimer } from 'react-idle-timer'
import App from './App'
export default function (props) {
const handleOnIdle = event => {
console.log('user is idle', event)
console.log('last active', getLastActiveTime())
}
const handleOnActive = event => {
console.log('user is active', event)
console.log('time remaining', getRemainingTime())
}
const handleOnAction = event => {
console.log('user did something', event)
}
const { getRemainingTime, getLastActiveTime } = useIdleTimer({
timeout: 1000 * 60 * 15,
onIdle: handleOnIdle,
onActive: handleOnActive,
onAction: handleOnAction,
debounce: 500
})
return (
<div>
{/* your app here */}
</div>
)
}

React Functional Component's state gets reset with setTimeout

Can someone please help me understand why this variable requestCount gets anchored at 0? I expect that it will increment every 3s but instead it doesn't.
const SomeComponent = () => {
const [requestCount, setRequestCount] = useState(-1);
const doSomeWork= () => {
setRequestCount(requestCount + 1);
setTimeout(doSomeWork, 3000);
};
return (
<>
<button onClick={doSomeWork}>Click</button>
{requestCount}
</>
);
};
Some knowledge should be known first: Why am I seeing stale props or state inside my function?
You have two options:
Use Functional updates:
Use useCallback with dependencies to create new doSomeWork every time when the requestCount state changes. So that the doSomeWork will read the latest requestCount state instead of stale value. And, you should execute the macro task queued by setTimeout using useEffect() hook with doSomeWork dependency. Because we need to wait for the execution of useCallback to complete and create a new doSomeWork function so that the macro task(doSomeWork) will read the latest requestCount.
The below example shows these two options:
SomeComponent.tsx:
import React, { useCallback, useEffect, useRef } from 'react';
import { useState } from 'react';
export const SomeComponent = () => {
const [requestCount, setRequestCount] = useState(-1);
// should execute macro task queued by setTimeout
const ref = useRef(null);
// option 1
// const doSomeWork = () => {
// setRequestCount((pre) => pre + 1);
// setTimeout(doSomeWork, 3000);
// };
// option 2
const doSomeWork = useCallback(() => {
setRequestCount(requestCount + 1);
if (ref.current) return;
ref.current = true;
}, [requestCount]);
useEffect(() => {
if (ref.current) {
setTimeout(doSomeWork, 3000);
}
}, [ref.current, doSomeWork]);
return (
<>
<button onClick={doSomeWork}>Click</button>
{requestCount}
</>
);
};
Unit test:
import { render, screen, fireEvent, act } from '#testing-library/react';
import React from 'react';
import { SomeComponent } from './SomeComponent';
describe('70297876', () => {
test('should pass', async () => {
jest.useFakeTimers();
const { container } = render(<SomeComponent />);
expect(container).toMatchInlineSnapshot(`
<div>
<button>
Click
</button>
-1
</div>
`);
const button = screen.getByText('Click');
fireEvent.click(button);
act(() => {
jest.advanceTimersByTime(3000);
});
expect(container).toMatchInlineSnapshot(`
<div>
<button>
Click
</button>
1
</div>
`);
act(() => {
jest.advanceTimersByTime(3000);
});
expect(container).toMatchInlineSnapshot(`
<div>
<button>
Click
</button>
2
</div>
`);
});
});
Test result:
PASS stackoverflow/70297876/SomeComponent.test.tsx
70297876
✓ should pass (36 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 3 passed, 3 total
Time: 3.865 s, estimated 4 s

Rerender component after function in provider ends

I'm trying to set components with 3 functionalities. Displaying PokemonList, getting random pokemon and find one by filters. Getting random pokemon works great but since 2 days I'm trying to figure out how to set pokemon list feature correctly
Below full code from this component.
It's render when click PokemonsList button inside separate navigation component and fire handleGetPokemonList function in provider using context.
The problem is that I can't manage rerender components when PokemonList is ready. For now i need to additionally fire forceUpadte() function manually (button onClick = () => forceUpdate())
I tried to use useEffect() in PokemonList component but it didn't work in any way.
I was also sure that after fetching data with fetchData() function I can do .then(changeState of loading) but it didn't work also.
What Am I missing to automatically render data from fetch in provider in PokemonList component? I'm receiving error about no data exist but if I use forceUpdate then everything is ok
Complete repo here: https://github.com/Mankowski92/poke-trainer
handleGetPokemonList function in provider below
const handleGetPokemonList = () => {
setCurrentPokedexOption('pokemonList');
async function fetchData() {
setImgLoaded(false);
let res = await fetch(`${API}?offset=0&limit=6/`);
let response = await res.json();
response.results.forEach((item) => {
const fetchDeeper = async () => {
let res = await fetch(`${item.url}`);
let response = await res.json();
let eachPoke = {
id: response.id,
name: response.name,
artwork: response.sprites.other['officialartwork'].front_default,
stats: response.stats,
};
fetchedPokemons.push(eachPoke);
};
fetchDeeper();
});
setPokemonList(fetchedPokemons);
if (fetchedPokemons) {
return setLoading(false);
}
}
fetchData()
.then((res) => setLoading(res))
.catch((err) => console.log('error', err));
};
PokemonList component below
import React, { useContext, useState, useCallback } from 'react';
import { StyledPokemonListContainer } from './PokemonList.styles';
import { PokemonsContext } from '../../../providers/PokemonsProvider';
const PokemonList = () => {
const ctx = useContext(PokemonsContext);
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const { handleSetImgLoaded } = useContext(PokemonsContext);
return (
<>
{ctx.currentPokedexOption === 'pokemonList' ? (
<StyledPokemonListContainer>
{ctx.pokemonList && ctx.pokemonList.length ? (
ctx.pokemonList.map((item, i) => (
<div className="each-pokemon-container" key={i}>
<div className="poke-id">{item.id}</div>
<div className="poke-name">{item.name}</div>
<img className="poke-photo" onLoad={() => handleSetImgLoaded()} src={item ? item.artwork : ''} alt="" />
</div>
))
) : (
<div className="render-info">Hit rerender button</div>
)}
{/* {ctx.pokemonList ? <div>{ctx.pokemonList[0].name}</div> : <div>DUPPSKO</div>} */}
<div className="buttons">
<button onClick={() => console.log('PREVOIUS')}>Previous</button>
<button className="rerender-button" onClick={() => forceUpdate()}>
RERENDER
</button>
<button onClick={() => console.log('NEXT')}>Next</button>
</div>
</StyledPokemonListContainer>
) : null}
</>
);
};
export default PokemonList;

Counter increment on setInterval

import React from 'react';
import {Plugins} from '#capacitor/core';
import {useState, useEffect} from 'react';
import {db} from './Firebase';
const Maps = () => {
const [lat, setLat] = useState(0);
const [long, setLong] = useState(0);
const [count, setCount] = useState (0);
const Counter = () => {
setCount(count + 1)
console.log(count)
}
const Location = () => {
Plugins.Geolocation.getCurrentPosition().then(
result => setLat ( result.coords.latitude)
)
Plugins.Geolocation.getCurrentPosition().then(
result => setLong (result.coords.longitude)
)
}
const interval = () => {
setInterval (() =>
{
Location();
Counter();
}, 5000 );
}
return (
<div>
<div>
<button onClick = {interval}>
Get Location
</button>
</div>
<div>
{long}
</div>
<div>
{lat}
</div>
</div>
)
}
export default Maps;
I'm trying to get the counter to increment on every iteration of setInterval, through the counter function, but when I log count, it does not increment and always remains as 0.
I've tried running setCount itself within setInterval without any success, it still does not increment count.
Its a stale closure. Change to this setCount(prevCount => prevCount + 1).
Using the updater form of set state like above, you can guarantee that you will be using the most recent value of state.
You can think of it as count in your function being a snapshot of what its value was when the setInterval was declared. This will stop your updates from appearing to work.
In addition, setting state is async, so the console.log(count) will most likely not reflect the new value. Log in an effect or outside the function body to see the updated value each render.
A note about your implementation:
You are creating a setInterval each time the button is clicked. This could lead to some interesting side-effects if clicked more than once. If you click the button twice for example, you will have two setIntervals running every 5 seconds.
In addition to #BrianThompson answer. Try this to avoid innecessary rerenders
import React from 'react';
import {Plugins} from '#capacitor/core';
import {useState, useEffect} from 'react';
import {db} from './Firebase';
const Maps = () => {
const [state, setState] = useState({
latLng:{lat:0,lng:0},
counter: 0
})
const interval = useRef()
//Use camelCase for methods
const location = () => {
Plugins.Geolocation.getCurrentPosition().then(
result => setState ( ({counter}) => {
counter = counter+1
console.log(counter)
return ({
latLng: {
lat: result.coords.latitude,
lng: result.coords.longitude
},
counter
})
})
)
}
const startInterval = () => {
if(interval.current) return;
interval.current = setInterval (() => {
location();
}, 5000 );
}
const stopInterval = () ={
clearInterval(interval.current)
interval.current = null
}
useEffect(()=>{
//Because interval is causing state updates, remember to clear interval when component will unmount
return stopInterval
},[])
return (
<div>
<div>
<button onClick = {startInterval}>
Get Location
</button>
</div>
<div>
{state.latLng.lng}
</div>
<div>
{state.latLng.lat}
</div>
</div>
)
}

Resources