I wrote a component that uses Player from #vimeo/player but now I am unsure how to write tests that are not focused on implementation details. I am using testing-library.
Specifically, I am looking for a way to test state changes that stem from these events (loaded, bufferstart, bufferend, play, pause):
useEffect(() => {
async function setupPlayer() {
if (playerRef.current) {
playerRef.current.on('loaded', () => setIsLoading(false));
playerRef.current.on('bufferstart', () => setIsBuffering(true));
playerRef.current.on('bufferend', () => setIsBuffering(false));
playerRef.current.on('play', () => setIsPlaying(true));
playerRef.current.on('pause', () => setIsPlaying(false));
}
}
if (playerRef.current) {
playerRef.current = new Player(
playerRef.current,
{ url: `https://player.vimeo.com/video/${video}`,
loop: true,
title: false,
portrait: false,
controls: false,
byline: false,
muted: true,
responsive: true },
);
setupPlayer();
}
return () => playerRef.current && playerRef.current.destroy();
}, [playerRef]);
My goal is to test for example that a loading/buffering has been rendered until loaded/bufferend events are fired.
/* eslint-disable max-len */
import React, { useEffect, useRef, useState } from 'react';
import Button from 'button';
import { IconScan, IconPause, IconPlay } from 'iconography';
import Loader from 'loader';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Player from '#vimeo/player';
const VIDEO_PLAYER_INDEX = 2;
const VideoPlayer = ({ wrapperEl,
video,
style,
wrapperClassNames,
renderedInOverlay,
onSetOverlay,
isMobile }) => {
const playerRef = useRef();
const [isLoading, setIsLoading] = useState(true);
const [isPlaying, setIsPlaying] = useState(false);
const [isHovering, setIsHovering] = useState(true);
const [isBuffering, setIsBuffering] = useState(false);
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
useEffect(() => {
async function setupPlayer() {
if (playerRef.current) {
playerRef.current.on('loaded', (e) => {
setIsLoading(false);
console.log(e);
});
playerRef.current.on('bufferstart', () => setIsBuffering(true));
playerRef.current.on('bufferend', () => setIsBuffering(false));
playerRef.current.on('play', () => setIsPlaying(true));
playerRef.current.on('pause', () => setIsPlaying(false));
}
}
if (playerRef.current) {
playerRef.current = new Player(
playerRef.current,
{ url: `https://player.vimeo.com/video/${video}`,
loop: true,
title: false,
portrait: false,
controls: false,
byline: false,
muted: true,
responsive: true },
);
setupPlayer();
}
return () => playerRef.current && playerRef.current.destroy();
}, [playerRef]);
useEffect(() => {
if (isMobile && isPlaying && !isBuffering) {
setIsHovering(false);
}
}, [isPlaying, isMobile, isBuffering]);
useEffect(() => {
if (playerRef.current) {
Promise.all([playerRef.current.getVideoWidth(), playerRef.current.getVideoHeight()]).then((dimensions) => {
setWidth(dimensions[0]);
setHeight(dimensions[1]);
});
}
}, [isMobile, playerRef]);
const handlePlayClick = () => {
if (isPlaying) {
playerRef.current.pause();
} else if (!isPlaying && !isBuffering) {
playerRef.current.play();
}
};
const renderIframe = () => {
if (!video) return null;
const showControls = isHovering || !isPlaying || isBuffering;
const showPlay = isPlaying && !isBuffering;
const aspectRatio = (width / height) * 100;
const wrapperBtnStyle = renderedInOverlay ? { style: { maxWidth: `${aspectRatio}vh` } } : {};
return (
<div className={ classNames('video-player', { 'video-player--is-loading': isLoading }) }>
<Button
className="video-player__wrapper-btn"
variant="naked"
onClick={ () => handlePlayClick() }
onMouseEnter={ () => setIsHovering(true) }
onMouseLeave={ () => setIsHovering(false) }
{ ...wrapperBtnStyle }>
<div
ref={ playerRef }
className="video-player__player"
data-testid="video-player"
/>
{ showControls &&
<div className="video-player__controls-container">
<div className="video-player__controls-btn" >
{showPlay ?
<IconPause size="lg" title="Pause" className="video-player__control-icon" />
:
<IconPlay size="lg" title="Play" className="video-player__control-icon" />
}
</div>
{isBuffering && (
<div className="video-player__buffer-container">
<div className="video-player__buffer-indicator" />
</div>)}
</div>
}
</Button>
{ !renderedInOverlay &&
<div className="video-player__fullscreen-btn-container">
<Button
className="video-player__fullscreen-btn"
variant="naked"
onClick={ (e) => onSetOverlay(e, false, VIDEO_PLAYER_INDEX) }
onTouchStart={ (e) => onSetOverlay(e, false, VIDEO_PLAYER_INDEX) }>
<IconScan size="lg" title="Fullscreen" />
</Button>
</div>
}
<Loader className="video-player__loader" height={ 30 } />
</div>
);
};
return (
wrapperEl ?
React.createElement(
wrapperEl,
{
className: classNames(wrapperClassNames),
style,
},
renderIframe(),
)
: renderIframe()
);
};
VideoPlayer.defaultProps = {
video: '',
wrapperEl: '',
wrapperClassNames: '',
style: {},
renderedInOverlay: false,
isMobile: false,
};
VideoPlayer.propTypes = {
video: PropTypes.string.isRequired,
wrapperEl: PropTypes.string,
wrapperClassNames: PropTypes.string,
style: PropTypes.shape({}),
renderedInOverlay: PropTypes.bool,
isMobile: PropTypes.bool,
};
export {
VideoPlayer,
VIDEO_PLAYER_INDEX,
};
Is someone able to give me a pointer how I can achieve that? Thanks!
Related
My issue is that when I start a call for the first time I receive remote stream and the loadMetaData() works fine, but when I close the call and try to call the same peerId I receive the remote stream but the loadMetadata() doesn't emit any events.
MainContainer.tsx
import React, { useState, useEffect } from "react";
// #ts-ignore
import { Peer } from "peerjs";
import decodeJWT from "jwt-decode";
import callStates from "../helpers/callStates";
import { CALL_EVENTS } from "../helpers/callEvents";
import VideoBox from "./InCallView";
import OutgoingCallView from "./OutgoingCallView";
import IncomingCallView from "./IncomingCallView";
import StandbyView from "./StandbyView";
import { Box } from "#mui/material";
export interface Contacts {
firstName: string;
lastName: string;
peerId: string;
}
function MainContainer() {
const [callState, setCallState] = useState(callStates.standby);
const [contactsList, setContactsList] = useState<Contacts[]>([]);
const [callerState, setCallerState] = useState<"receiver" | "caller">();
const resetCallState = () => {
setCallState(callStates.standby);
};
useEffect(() => {
peer.on("connection", (conn: any) => {
conn.on("data", async (data: { event: string }) => {
console.log("EVENT TYPE:: ", data.event);
if (data.event === CALL_EVENTS.CALL_REQUEST) {
setCallerState("receiver");
if (callState === callStates.standby) {
setCallState(callStates.incomingCall);
}
}
if (data.event === CALL_EVENTS.ANSWERED_CALL) {
setCallerState("caller");
setCallState(callStates.inCall);
}
if (data.event === CALL_EVENTS.REJECT_CALL) {
resetCallState();
}
if (data.event === CALL_EVENTS.END_CALL) {
resetCallState();
}
});
});
// eslint-disable-next-line
}, [peer]);
const handleCall = async () => {
console.log("%c Call sent! ", "background: #222; color: #bada55; font-size: 25px");
const conn = await peer.connect(alaaId);
console.log({ conn });
setCallState(callStates.outgoingCall);
conn.on("open", () => {
console.log("%c connection opened! ", "background: #222; color: #bada55");
conn.send({ event: CALL_EVENTS.CALL_REQUEST });
});
};
const answerCall = async () => {
await setCallState(callStates.inCall);
const conn = peer.connect(alaaId);
conn.on("open", () => {
conn.send({ event: CALL_EVENTS.ANSWERED_CALL });
});
};
const rejectCall = () => {
const conn = peer.connect(alaaId);
conn.on("open", () => {
conn.send({ event: CALL_EVENTS.REJECT_CALL });
resetCallState();
});
};
const endCall = () => {
const conn = peer.connect(alaaId);
conn.on("open", () => {
resetCallState();
conn.send({ event: CALL_EVENTS.END_CALL });
});
};
const hangupCall = () => {
const conn = peer.connect(alaaId);
console.log({ conn });
conn.on("open", () => {
conn.send({ event: CALL_EVENTS.HANGUP_CALL });
resetCallState();
});
};
return (
<>
<Box sx={{ display: callState === callStates.standby ? "none" : "block" }}>
{callState === callStates.inCall && <VideoBox callState={callerState} peer={peer} peerId={alaaId} endCall={endCall} />}
{callState !== callStates.standby && callState !== callStates.inCall && (
<React.Fragment>
{callState === callStates.outgoingCall && <OutgoingCallView hangupCall={hangupCall} />}
{callState === callStates.incomingCall && <IncomingCallView answerCall={answerCall} rejectCall={rejectCall} />}
</React.Fragment>
)}
</Box>
{callState === callStates.standby && <StandbyView handleCall={handleCall} contactsList={contactsList} />}
</>
);
}
export default MainContainer;
InCallView.tsx
import { useDebugValue, useEffect, useState } from "react";
function useUserMedia(constraints: any) {
const [stream, setStream] = useState<MediaStream | null>();
const [error, setError] = useState();
useDebugValue({ error, stream });
useEffect(() => {
console.log("inside main useeffect");
let canceled = false;
navigator.mediaDevices.getUserMedia(constraints).then(
(stream) => {
if (!canceled) {
console.log("%c inside canceled condition", "font-size:60px");
setStream(stream);
}
},
(error) => {
if (!canceled) {
setError(error);
}
}
);
return () => {
canceled = true;
setStream(null);
};
}, [constraints]);
useEffect(
() => () => {
console.log("inside cancel useeffect");
if (stream)
stream.getTracks().forEach((track) => {
track.stop();
});
},
[stream]
);
return { error, stream };
}
export default useUserMedia;
useMediaStream hook
import { useState, useEffect } from "react";
export function useUserMedia() {
const [mediaStream, setMediaStream] = useState<MediaStream>();
useEffect(() => {
async function enableVideoStream() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
frameRate: { exact: 15, ideal: 15 },
echoCancellation: true,
noiseSuppression: true,
width: { min: 300, max: 640 },
height: { min: 300, max: 480 },
},
});
setMediaStream(stream);
} catch (err) {
console.log("Video error: ", err);
}
}
if (!mediaStream) {
enableVideoStream();
} else {
return function cleanup() {
mediaStream.getTracks().forEach((track) => {
track.stop();
});
};
}
// eslint-disable-next-line
}, [mediaStream]);
return mediaStream;
}
I trie many solutions but non of them works with me
This is a game that I am building and in this game I have created some levels. But when the game ends for the first time the second time my countdown timer is not getting decremented.
this is my App.js component:
import "./App.css";
import React, { useState, useEffect, useCallback } from "react";
import SingleCard from "./components/singleCard/SingleCard";
import Timer from "./components/timer/Timer";
import Modal from "./components/modal/Modal";
import soundOn from "./images/soundon.png";
import soundOff from "./images/soundoff.png";
import { Helmet } from "react-helmet";
const cardImages = [
{ src: "/img/img-1.png", matched: false },
{ src: "/img/img-2.png", matched: false },
{ src: "/img/img-3.png", matched: false },
{ src: "/img/img-4.png", matched: false },
{ src: "/img/img-5.png", matched: false },
{ src: "/img/img-6.png", matched: false },
];
function App({ background, wrongAns, correctAns, deadSound, winSound }) {
const [cards, setCards] = useState([]);
const [turns, setTurns] = useState(0);
const [choiceOne, setChoiceOne] = useState(null);
const [choiceTwo, setChoiceTwo] = useState(null);
const [disabled, setDisabled] = useState(false);
const [isgameEnd, setGameEnd] = useState(false);
const [timerStart, setTimerStart] = useState(false);
const [playSound, setPlaySound] = useState(false);
const [count, setCount] = useState(0);
const [IsPlaying, setIsPlaying] = useState(true);
const [isModalOpen, setModalIsOpen] = useState(false);
const [restartTimer, setRestartTimer] = useState(false);
const [isMute, setMute] = useState(false);
const [loading, setLoading] = useState(true);
function handleMute(state = false) {
background.muted = state;
wrongAns.muted = state;
correctAns.muted = state;
deadSound.muted = state;
winSound.muted = state;
setMute(state);
}
let timer;
// function that will decide the condition for opening the modal
const toggleModal = () => {
setModalIsOpen(true);
};
// function that will execute when we click a button in the modal
const handlePlaySound = () => {
setPlaySound(false);
};
// function that will execute when game is set to background in android
function AudioBgOnPause() {
if (playSound === true) {
background.pause();
setIsPlaying(false);
}
}
// functiona that will execute when game is again resumed
function AudioBgOnResume() {
if (IsPlaying === false) {
setIsPlaying(true);
}
}
// creating there global reference so that we can call these functions in the index file
window.AudioBgOnPause = AudioBgOnPause;
window.AudioBgOnResume = AudioBgOnResume;
// check if playSound is off or on
if (playSound === false) {
background.pause();
} else if (playSound === true && IsPlaying === true) {
background.play();
}
// Play Again
const playAgain = () => {
// setCards([]);
shuffleCards();
setTurns(0);
setChoiceOne(null);
setChoiceTwo(null);
setDisabled(false);
setGameEnd(false);
setTimerStart(false);
setPlaySound(false);
setCount(0);
setIsPlaying(true);
setModalIsOpen(false);
setRestartTimer(true);
setMute(false);
};
const restartGame = () => {
playAgain();
};
// check if isGameEnd is true i.e. the game is ended
// losing condition
useEffect(() => {
if (turns < 6 && isgameEnd === true) {
setDisabled(true);
setTimerStart(false);
clearInterval(timer);
if (playSound === true) {
deadSound.play();
}
setPlaySound(false);
setTimeout(() => {
toggleModal();
}, 2000);
}
}, [turns, isgameEnd]);
// winning situation
useEffect(() => {
if (
(turns === 6 && isgameEnd === false) ||
(turns === 6 && isgameEnd === true)
) {
// clearInterval(timer);
// setDisabled(true);
setRestartTimer(true);
setTimerStart(false);
if (playSound === true) {
winSound.play();
}
setPlaySound(playSound);
shuffleCards();
// setTimeout(() => {
// toggleModal();
// }, 2000);
}
}, [turns, isgameEnd]);
// shuffle Cards
const shuffleCards = () => {
const shuffleCards = [...cardImages, ...cardImages]
.sort(() => Math.random() - 0.5)
.map((card) => ({ ...card, id: Math.random() }));
setCards(shuffleCards);
setTurns(0);
};
// console.log("cards array", cards);
// handle a choice
const handleChoice = (card) => {
setTimerStart(true);
// background.play();
background.loop = true;
// checking if the counter is one only then set sound to true when the card is flipped for first time
count === 1 ? setPlaySound(true) : setPlaySound(playSound);
// after that increment the counter so that the upper condition should not hold anymore
setCount(count + 1);
choiceOne ? setChoiceTwo(card) : setChoiceOne(card);
};
// compare 2 selected cards
useEffect(() => {
if (choiceOne && choiceTwo) {
setDisabled(true);
if (choiceOne.src === choiceTwo.src) {
setCards((prevCards) => {
return prevCards.map((card) => {
if (card.src === choiceOne.src) {
return { ...card, matched: true };
} else {
return card;
}
});
});
if (playSound === true) {
correctAns.play();
}
setTurns((prevTurns) => prevTurns + 1);
resetTurn();
} else {
if (playSound === true) {
wrongAns.play();
}
setTimeout(() => resetTurn(), 500);
}
}
}, [choiceOne, choiceTwo]);
// start a new game automatically
// set counter to one when the component first mounts so that sound starts to play on first click only
useEffect(() => {
shuffleCards();
setCount(count + 1);
}, []);
// reset choices
const resetTurn = () => {
setChoiceOne(null);
setChoiceTwo(null);
setDisabled(false);
};
// console.log("restart App", restartTimer);
// timer callback
const onGameEnd = useCallback(() => {
setGameEnd(!isgameEnd);
}, [isgameEnd]);
useEffect(() => {
setTimeout(() => {
setLoading(false);
}, 4000);
}, []);
return (
<>
<Helmet>
<meta charSet="utf-8" />
<title>Match Maker</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
</Helmet>
<div className="App">
{loading && (
<div className="loader-container">
<div className="spinner"></div>
</div>
)}
<>
{/* <img
className="logo"
src="https://cheetay.pk/static/images/newLandingPage/logo.svg"
alt="card back"
/> */}
<div
style={
loading ? { visibility: "hidden" } : { visibility: "inherit" }
}
>
<div className="soundBtn">
{!isMute === true ? (
<div
className="soundIcon"
style={{
cursor: "pointer",
khtmlUserSelect: "none",
MozUserSelect: "none",
OUserSelect: "none",
userSelect: "none",
}}
onClick={() => handleMute(!isMute)}
>
<img src={soundOn} alt="soundOff" />
</div>
) : (
<div
className="soundIcon"
style={{
cursor: "pointer",
khtmlUserSelect: "none",
MozUserSelect: "none",
OUserSelect: "none",
userSelect: "none",
}}
onClick={() => handleMute(!isMute)}
>
<img src={soundOff} alt="soundOff" />
</div>
)}
</div>
<div className="card-grid">
{cards.map((card) => (
<SingleCard
key={card.id}
card={card}
handleChoice={handleChoice}
flipped={
card === choiceOne || card === choiceTwo || card.matched
}
disabled={disabled}
isModalOpen={isModalOpen}
/>
))}
</div>
<div className="TimerAndTurnsInfo">
<Timer
timerStart={timerStart}
timer={timer}
onGameEnd={onGameEnd}
restartTimer={restartTimer}
/>
<p>matched {turns}</p>
</div>
</div>
</>
</div>
{isModalOpen && (
<Modal handlePlaySound={handlePlaySound} restartGame={restartGame} />
)}
</>
);
}
export default App;
and this is my timer component:
import React, { useEffect, useState } from "react";
import "./Timer.css";
const Child = ({ timerStart, timer, onGameEnd, restartTimer }) => {
const [seconds, setSeconds] = useState(40);
// let time = 40;
useEffect(() => {
if (restartTimer === true) {
setSeconds(40);
}
}, [seconds, restartTimer]);
// console.log("restart Timer", restartTimer);
useEffect(() => {
if (timerStart === true) {
timer = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
if (seconds === 0) {
onGameEnd(true);
clearInterval(timer);
}
}, 1000);
}
return () => clearInterval(timer);
});
return (
<p className="time">
Time{" "}
<span className="span1">
<span>{seconds}s</span>
</span>
</p>
);
};
const Timer = React.memo(({ timerStart, timer, onGameEnd, restartTimer }) => (
<Child
timerStart={timerStart}
timer={timer}
onGameEnd={onGameEnd}
restartTimer={restartTimer}
/>
));
export default Timer;
my timer gets re initialized when restartTimer state is set to true.
I think your problem is here
useEffect(() => {
if (restartTimer === true) {
setSeconds(40);
}
}, [seconds, restartTimer]);
whenever the seconds reduce and restartTimer === true (can't find where you set it to false after useState) you reset it to 40.
try removing seconds from the dependency
I have a big problem with React TimeLines Package(https://openbase.com/js/react-timelines)
I want something like this photo:
( having 3 P tags with different ClassNames)
but in default case of this package I cant do it!
I think I should use something like createElement and textContent in JS. but I dont know how!
My Codes:
import React, { Component } from "react";
import Timeline from "react-timelines";
import "react-timelines/lib/css/style.css";
import { START_YEAR, NUM_OF_YEARS, NUM_OF_TRACKS } from "./constant";
import { buildTimebar, buildTrack } from "./builder";
import { fill } from "./utils";
const now = new Date("2021-01-01");
const timebar = buildTimebar();
// eslint-disable-next-line no-alert
const clickElement = (element) =>
alert(`Clicked element\n${JSON.stringify(element, null, 2)}`);
class App extends Component {
constructor(props) {
super(props);
const tracksById = fill(NUM_OF_TRACKS).reduce((acc, i) => {
const track = buildTrack(i + 1);
acc[track.id] = track;
return acc;
}, {});
this.state = {
open: false,
zoom: 2,
// eslint-disable-next-line react/no-unused-state
tracksById,
tracks: Object.values(tracksById),
};
}
handleToggleOpen = () => {
this.setState(({ open }) => ({ open: !open }));
};
handleToggleTrackOpen = (track) => {
this.setState((state) => {
const tracksById = {
...state.tracksById,
[track.id]: {
...track,
isOpen: !track.isOpen,
},
};
return {
tracksById,
tracks: Object.values(tracksById),
};
});
};
render() {
const { open, zoom, tracks } = this.state;
const start = new Date(`${START_YEAR}`);
const end = new Date(`${START_YEAR + NUM_OF_YEARS}`);
return (
<div className="app">
<Timeline
scale={{
start,
end,
zoom,
}}
isOpen={open}
toggleOpen={this.handleToggleOpen}
clickElement={clickElement}
timebar={timebar}
tracks={tracks}
now={now}
enableSticky
scrollToNow
/>
</div>
);
}
}
export default App;
builder.js:
export const buildElement = ({ trackId, start, end, i }) => {
const bgColor = nextColor();
const color = colourIsLight(...hexToRgb(bgColor)) ? "#000000" : "#ffffff";
return {
id: `t-${trackId}-el-${i}`,
title: "Bye Title: Hello Type: String",
start,
end,
style: {
backgroundColor: `#${bgColor}`,
color,
borderRadius: "12px",
width: "auto",
height: "120px",
textTransform: "capitalize",
},
};
};
I have an app with React front and Spring backend. I use Axios to fetch from the back. I have 2 class components with tables and I can access them via a menu component (in componentDidMount and componentDidUpdate only). I use all the possible precautions against infinite loops (loaded state and isMounted with a custom name). It works in the first component which I access after logging in. However, the second component (which is logically the same as the first, just has another entity to fetch) keeps requesting with axios until i go there (i see it in the network tab of my browser). Why can it be? it is definitely not mounted and console.logs don't work from there but while I'm on first it keeps requesting on and on (and it doesn't receive anything I guess, it is 0 bytes at this time)
import React, { Component } from 'react'
import {Link} from 'react-router-dom';
import axios from 'axios'
import "react-table/react-table.css";
import ReactTable from 'react-table';
import {Button, ButtonToolbar} from 'react-bootstrap';
import { LinkContainer } from "react-router-bootstrap";
import AddCalculationsModal from './AddCalculationsModal';
import UpdateCalculationsModal from './UpdateCalculationsModal';
import Cluster from './Cluster';
import Select from 'react-select/src/Select';
export default class Calculations extends Component {
isCMounted = false;
constructor(props) {
super(props)
this.state = {
items: [],
selected: null,
addModalShow: false,
updateModalShow: false,
updateId: null,
buttonOn: false,
page: 0,
elements: 0,
loaded: false
}
}
componentDidMount() {
this.isCMounted = true;
if(!this.state.loaded){
this.load();
}
};
componentDidUpdate() {
if(!this.state.loaded){
this.load();
}
};
componentWillUnmount(){
this.isCMounted = false;
}
increasePage = () => {
this.setState({
page: this.state.page + 1
})
}
decreasePage = () => {
this.setState({
page: this.state.page - 1
})
}
load = async () => {
await axios.get(`calculations?page=${this.state.page}&elements=${this.state.elements}`)
.then(res => {
if (this.isCMounted && this.state.items.id === res.data.id){
this.setState({items: res.data})
}
});
if(this.state.selected != null && this.isCMounted) {
this.setState({buttonOn: true})
}
this.setState({loaded: true})
}
setId = (id) => {
const idValue = this.state.items[id].id;
if (this.isCMounted)
this.setState({updateId: idValue});
}
deleteRow = (id) => {
const index = this.state.items.findIndex(item => {
return item.id === this.state.items[id].id})
const idValue = this.state.items[id].id
axios.delete(`calculations/${idValue}`).then(
res => {
this.load();
}
)
this.state.items.splice(index, 1)
this.load();
}
render() {
let addModalClose = () => this.setState({addModalShow: false});
let updateModalClose = () => this.setState({updateModalShow: false});
return (
<div>
<h3>Calculations</h3>
<ReactTable
columns={
[
{
Header: "ID",
accessor: "id"
},
{
Header: "Name",
accessor: "name"
},
{
Header: "Creation Date",
accessor: "dateCreate"
},
{
Header: "Update Date",
accessor: "dateUpdate"
},
{
Header: "User",
accessor: "userId"
}
]
}
data={this.state.items}
filterable
showPagination={false}
getTrProps={(state, rowInfo) => {
if (rowInfo && rowInfo.row) {
return {
onClick: (e) => {
this.setState({
selected: rowInfo.index
})
},
style: {
background: rowInfo.index === this.state.selected ? '#00afec' : 'white',
color: rowInfo.index === this.state.selected ? 'white' : 'black'
}
}
}else{
return {}
}
}}
>
</ReactTable>
<ButtonToolbar>
<Button variant="primary" onClick={() => {
this.decreasePage();
this.load();
}}>PREVIOUS PAGE</Button>
<Button variant="primary" onClick={() => {
this.increasePage();
this.load();
}}>NEXT PAGE</Button>
</ButtonToolbar>
<ButtonToolbar>
<Button variant="primary" onClick={() => this.setState({addModalShow: true})}>
Add Calculation
</Button>
<Button variant="primary" onClick={() => {
this.setId(this.state.selected);
this.setState({updateModalShow: true})}} disabled={this.state.buttonOn ? false : true}>
Update Calculation
</Button>
<Button variant="danger" onClick={() => {
this.deleteRow(this.state.selected);
}}>DELETE</Button>
<Link to={`/calculations/${this.state.items[this.state.selected] && this.state.items[this.state.selected].id}`}>
<Button variant="warning" disabled={this.state.buttonOn ? false : true}>Cluster</Button>
</Link>
<AddCalculationsModal
show={this.state.addModalShow}
onHide={addModalClose}
calculation={this.state.items[this.state.selected]}
/>
<UpdateCalculationsModal
show={this.state.updateModalShow}
onHide={updateModalClose}
calculation={this.state.items[this.state.selected] && this.state.items[this.state.selected].id}
calcname={this.state.items[this.state.selected] && this.state.items[this.state.selected].name}
/>
</ButtonToolbar>
</div>
)
}
}
And
import React, { Component } from 'react'
import axios from 'axios'
import "react-table/react-table.css";
import ReactTable from 'react-table';
import {Button, ButtonToolbar} from 'react-bootstrap';
import AuthenticationService from '../service/AuthenticationService';
export default class Calculations extends Component {
isCMounted = false;
constructor(props) {
super(props)
this.state = {
items: [],
selected: null,
updateId: null,
loaded: false
}
}
componentDidMount() {
this.isCMounted = true;
if(!this.state.loaded) {
this.load();
}
};
componentDidUpdate() {
if(!this.state.loaded) {
this.load();
}
};
componentWillUnmount() {
this.isCMounted = false;
}
load = async () => {
if(this.isCMounted && !this.state.loaded) {
await axios.get('calculation-types')
.then(res => {
console.log(this.isCMounted)
if (this.isCMounted && this.state.items.id === res.data.id){
this.setState({items: res.data})
}
});
this.setState({loaded: true})
}
}
setId = (id) => {
const idValue = this.state.items[id].id;
if (this.isCMounted)
this.setState({updateId: idValue});
}
render() {
return (
<div>
<h3>Calculation Types</h3>
<ReactTable
columns={
[
{
Header: "ID",
accessor: "idType",
width: 100,
minWidth: 100,
maxWidth: 100
},
{
Header: "Name",
accessor: "name"
}
]
}
data={this.state.items}
filterable
showPagination={false}
getTrProps={(state, rowInfo) => {
if (rowInfo && rowInfo.row) {
return {
onClick: (e) => {
this.setState({
selected: rowInfo.index
})
},
style: {
background: rowInfo.index === this.state.selected ? '#00afec' : 'white',
color: rowInfo.index === this.state.selected ? 'white' : 'black'
}
}
}else{
return {}
}
}}
>
</ReactTable>
</div>
)
}
}
are my components. Menu is a normal link. after login i appear on the first with menu on top.
Have you tried moving this.setState({loaded: true}) into the axios response callback block? Since you're awaiting the fetch request, I wonder if the this.setState({items: res.data} that you have in the callback block is causing an infinite componentDidUpdate loop that causes load to be repeatedly called without ever having the chance to arrive at the this.setState({loaded: true}) in the final line of load.
load = async () => {
if(this.isCMounted && !this.state.loaded) {
await axios.get('calculation-types')
.then(res => {
console.log(this.isCMounted)
if (this.isCMounted && this.state.items.id === res.data.id){
this.setState({ items: res.data, loaded: true })
}
});
}
}
I have some project that uses antd, redux, saga,
In particular I use antd autocomplete https://ant.design/components/auto-complete/
I am trying to make antd autocomplete work with server data and send redux action each time when user types something in input. The problem is I dont know how to make it work with redux saga.
Supposedly I get data from server succesfully and I update store with that data. So how to show this data in dropdown. Dropdown data should probably change each time user types
const {createStore, applyMiddleware} = Redux;
const {Provider, connect} = ReactRedux;
const { createAction, handleActions } = window.ReduxActions;
const createSagaMiddleware = ReduxSaga.default;
const {takeEvery, takeLatest, put, call} = ReduxSaga.effects;
const { AutoComplete } = antd;
const { Option, OptGroup } = AutoComplete;
const initialState = {
isLoading: false,
list: [],
};
var dataSource = [
{
"directionId": "3e8a6a23-d325-4c74-9bb5-e83c0bd4de4e",
"directionName": "ABC",
"items": [
{
"docflowType": 71,
"docflowTypeName": "infoABC",
"startDate": "2019-10-07T09:47:32.119004",
"endDate": "2019-10-07T09:47:32.119004",
"count": 1
}
]
},
{
"directionId": "7feb83e0-4d7b-4d99-8adb-23c25473d57d",
"directionName": "TGK",
"items": [
{
"docflowType": 74,
"docflowTypeName": "reportG",
"startDate": "2019-10-07T09:42:08.549327",
"endDate": "2019-10-07T09:42:08.549327",
"count": 1
}
]
}
]
function renderTitle(title) {
return (
<span>{title}</span>
);
}
const options = dataSource.map(group => (
<OptGroup key={group.directionId} label={renderTitle(group.directionName)}>
{group.items.map(opt => (
<Option key={opt.docflowTypeName} value={opt.docflowTypeName}>
{opt.docflowTypeName}
<span className="certain-search-item-count"> {opt.count} организаций</span>
</Option>
))}
</OptGroup>
));
const fetchSuggest = createAction('FETCH_SUGGEST_REQUEST', payload => payload);
const fetchSuggestSuccess = createAction('FETCH_SUGGEST_SUCCESS', result => result);
const fetchSuggestError = createAction('FETCH_SUGGEST_ERROR');
const autoSuggesting = handleActions(
{
[fetchSuggest]: (state, action) => ({
...state,
isLoading: true,
}),
[fetchSuggestSuccess]: (state, action) => {
console.log("fetchSuggestSuccess", action);
return {
...state,
isLoading: false,
list: action.payload,
}},
[fetchSuggestError]: (state, action) => ({
...state,
isLoading: false,
})
},
initialState
);
const fetchList = async payload => {
var {filter, exclusion} = payload || {};
const response = await axios.get(`http://someserver.com?filter=${filter}&exceptTestCodes=${exclusion}`);
return response.data;
};
function* fetchSuggestion(action) {
console.log("worker saga получает", action.payload)
try {
const data = action.payload;
const response = yield call(fetchList, data);
console.log(response);
// yield delay(200);
yield put(fetchSuggestSuccess(response));
} catch (error) {
yield put(fetchSuggestError());
}
}
function* watchInput() {
yield takeLatest('FETCH_SUGGEST_REQUEST', fetchSuggestion);
}
const sagaMiddleware = createSagaMiddleware();
const store = createStore(autoSuggesting, initialState, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(watchInput);
class App extends React.Component {
constructor(props) {
super(props);
this.state = { value: '', dataSource:[] };
}
handleSearch = searchText => {
this.setState({
dataSource: !searchText ? [] : this.getListToDisplay(),
});
}
handleChange = value => {
this.setState({ value });
this.props.fetchSuggest({filter:value, exclusion: false});
}
getListToDisplay = () => {
return this.props.list.map(group => (
<OptGroup key={group.directionId} label={renderTitle(group.directionName)}>
{group.items.map(opt => (
<Option key={opt.docflowTypeName} value={opt.docflowTypeName}>
{opt.docflowTypeName}
<span className="certain-search-item-count"> {opt.count} организаций</span>
</Option>
))}
</OptGroup>
))
}
render() {
return (
<React.Fragment>
<AutoComplete
value={this.state.value}
onChange={this.handleChange}
onSearch={this.handleSearch}
onSearch={this.handleSearch}
dataSource={this.getListToDisplay()}
optionLabelProp="value"
dropdownMatchSelectWidth={false}
dropdownStyle={{ width: 405 }}
style={{ width: '30%', overflow: 'hidden' }}
// filterOption
>
</AutoComplete>
</React.Fragment>
);
}
}
const mapStateToProps = state => state;
const mapDispatchToProps = dispatch => ({ fetchSuggest: (data) => dispatch(fetchSuggest(data)) });
const ConnectedApp = connect(mapStateToProps, mapDispatchToProps)(App);
ReactDOM.render(<Provider store={store}><ConnectedApp/></Provider>, document.getElementById('root'));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.0.3/react-redux.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/redux#latest/dist/redux.js"></script>
<script src="https://unpkg.com/redux-actions#latest/dist/redux-actions.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-saga/1.0.2/redux-saga.umd.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/babel-standalone#latest/babel.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.4.4/polyfill.min.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/moment/min/moment-with-locales.js"></script>
<script src="https://unpkg.com/antd/dist/antd-with-locales.js"></script>
<link rel="stylesheet" href="https://unpkg.com/antd/dist/antd.css" />
This is given on the official documentation for ajax based dropdown
import { Select, Spin } from 'antd';
import debounce from 'lodash/debounce';
function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
const [fetching, setFetching] = React.useState(false);
const [options, setOptions] = React.useState([]);
const fetchRef = React.useRef(0);
const debounceFetcher = React.useMemo(() => {
const loadOptions = (value) => {
fetchRef.current += 1;
const fetchId = fetchRef.current;
setOptions([]);
setFetching(true);
fetchOptions(value).then((newOptions) => {
if (fetchId !== fetchRef.current) {
// for fetch callback order
return;
}
setOptions(newOptions);
setFetching(false);
});
};
return debounce(loadOptions, debounceTimeout);
}, [fetchOptions, debounceTimeout]);
return (
<Select
labelInValue
filterOption={false}
onSearch={debounceFetcher}
notFoundContent={fetching ? <Spin size="small" /> : null}
{...props}
options={options}
/>
);
} // Usage of DebounceSelect
async function fetchUserList(username) {
console.log('fetching user', username);
return fetch('https://randomuser.me/api/?results=5')
.then((response) => response.json())
.then((body) =>
body.results.map((user) => ({
label: `${user.name.first} ${user.name.last}`,
value: user.login.username,
})),
);
}
const Demo = () => {
const [value, setValue] = React.useState([]);
return (
<DebounceSelect
mode="multiple"
value={value}
placeholder="Select users"
fetchOptions={fetchUserList}
onChange={(newValue) => {
setValue(newValue);
}}
style={{
width: '100%',
}}
/>
);
};
ReactDOM.render(<Demo />, mountNode);