Changing image based on index, using ImageId - reactjs

I'm using street views from Mapillary.js and depending on an image key, passed as a prop, I want to show different images/street views. I've tried to do it with a conditional (ternary) operator like this:
<Mapillary width="auto" height="94vh" imageId={currentClue === 0 ? '2978574139073965' : currentClue === 1 ? '461631028397375' : currentClue === 2 ? '2978574139073965' : currentClue === 3 ? '312627450377787' : currentClue === 4 ? '695710578427767' : ''} />
Right now though, I only see the first image (when currentClue === 0). When currentClue === 1, the first image is still showing, although I can see in the console that the currentClue index is ascending.
I've also tried to change image by saving imageId in state and using it like this:
const handleClick = () => {
setCurrentClue(currentClue + 1);
if (currentClue < 4) { //* Show alert if clue index > 4
setLevel(level - 1);
if (currentClue === 1) setImageId('461631028397375')
if (currentClue === 2) setImageId('2978574139073965')
} else {
swal('Time to make a guess!', {
button: 'OK'
});
}
dispatch(game.actions.setScore(currentScore - 1));
};
But get the same result: First image is showing, but not the second.
Does anyone have any suggestions on how to get it to work?
This is the whole component where I'm trying to do this:
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { game } from 'reducers/game';
import swal from 'sweetalert';
import { Mapillary } from 'components/Mapillary/Mapillary';
import { Paragraph } from 'GlobalStyles';
import { MapillaryContainer, ClueContainer, SpecialSpan, ClueParagraph, AnotherClueButton } from './Clues.Styles'
export const Clues = () => {
const [games, setGames] = useState([])
const [loading, setLoading] = useState(false);
const [currentClue, setCurrentClue] = useState(0);
const [level, setLevel] = useState(5);
// const [imageId, setImageId] = useState('2978574139073965')
//* Fetching clues
const fetchClues = () => {
setLoading(true);
fetch('https://final-project-api-veooltntuq-lz.a.run.app/games')
.then((response) => {
return response.json()
})
.then((response) => {
setGames(response.games)
})
.catch((error) => console.error(error))
.finally(() => setLoading(false));
}
//* Setting current score
const currentScore = useSelector((store) => store.game.score);
const dispatch = useDispatch();
const handleClick = () => {
setCurrentClue(currentClue + 1);
if (currentClue < 4) { //* Show alert if clue index > 4
setLevel(level - 1);
/* if (currentClue === 1) setImageId('461631028397375')
if (currentClue === 2) setImageId('2978574139073965') */
} else {
swal('Time to make a guess!', {
button: 'OK'
});
}
dispatch(game.actions.setScore(currentScore - 1));
};
useEffect(() => {
fetchClues()
}, [])
const activeClue = games[currentClue];
if (loading) {
return <Paragraph>Loading clues...</Paragraph>
}
if (currentClue < 5) { //* Stop showing clues after clue 5
return (
<div>
<MapillaryContainer>
{console.log(currentClue)}
<Mapillary width="auto" height="94vh" imageId={currentClue === 0 ? '2978574139073965' : currentClue === 1 ? '461631028397375' : currentClue === 2 ? '2978574139073965' : currentClue === 3 ? '312627450377787' : currentClue === 4 ? '695710578427767' : ''} />
</MapillaryContainer>
<ClueContainer>
<SpecialSpan>Level: {level}</SpecialSpan>
<ClueParagraph>{activeClue && activeClue.gameOne}</ClueParagraph>
<AnotherClueButton type="button" onClick={() => handleClick()}>I need another clue</AnotherClueButton>
</ClueContainer>
</div>
)
}
}
EDIT: This is the Mapillary component:
import React, { Component } from 'react';
import { Viewer } from 'mapillary-js';
class ViewerComponent extends Component {
constructor(props) {
super(props);
this.containerRef = React.createRef();
}
componentDidMount() {
this.viewer = new Viewer({
accessToken: this.props.accessToken,
container: this.containerRef.current,
imageId: this.props.imageId
});
}
componentWillUnmount() {
if (this.viewer) {
this.viewer.remove();
}
}
render() {
return <div ref={this.containerRef} style={this.props.style} />;
}
}
export const Mapillary = (props) => {
return (
<ViewerComponent
accessToken={process.env.REACT_APP_MAPILLARY_CLIENT_TOKEN}
imageId={props.imageId}
style={{ width: props.width, height: props.height }} />
);
}

Well, in ViewerComponent you only create the viewer once, on component mount, so subsequent changes to it are not used anywhere.
Now if you read the docs for the imageId option, it states
Optional image-id to start from. The id can be any Mapillary image. If a id is provided the viewer is bound to that id until it has been fully loaded. If null is provided no image is loaded at viewer initialization and the viewer is not bound to any particular id. Any image can then be navigated to with e.g. viewer.moveTo("<my-image-id>").
So, you will have to add a componentDidUpdate(prevProps) to the ViewerComponent and check if the update has a new imageId and act accordingly.
componentDidUpdate(prevProps){
if (prevProps.imageId !== this.props.imageId) {
if (this.viewer) {
this.viewer.moveTo(this.props.imageId);
}
}
}

I think your second method will be work, the error is that you are adding and checking the currentClue value in the same function, your state is not updated, you have to use useEffect it will detect the change of currentClue state and change the image according to your state value, I hope it works.
here is one more thing you just need to handle the dispatch according to your requirements because useEffect will be called once your component load.
const handleClick = () => {
const val = currentClue + 1;
setCurrentClue(val);
};
useEffect(() => {
if (currentClue < 4) { //* Show alert if clue index > 4
setLevel(level - 1);
if (currentClue === 1) setImageId('461631028397375')
if (currentClue === 2) setImageId('2978574139073965')
} else {
swal('Time to make a guess!', {
button: 'OK'
});
}
dispatch(game.actions.setScore(currentScore - 1));
}, [currentClue])

Related

React Typewriter effect doesn't reset

I have created a typewriting effect with React and it works perfectly fine. However, when I change the language with i18n both texts don't have the same length and it keeps writing until both texts have the same length and then it changes the language and starts the effect again.
How can I reset the input when the language has changed? How can I reset the input when the component has been destroyed?
I have recorded a video
I have the same issue when I change from one page to another, as both pages have different texts and they don't have the same length.
Here code of my component
export const ConsoleText = ({text, complete = false}) => {
const [currentText, setCurrentText] = useState("");
const translatedText = i18n.t(text);
const index = useRef(0);
useEffect(() => {
if (!complete && currentText.length !== translatedText.length) {
const timeOut = setTimeout(() => {
setCurrentText((value) => value + translatedText.charAt(index.current));
index.current++;
}, 20);
return () => {
clearTimeout(timeOut);
}
} else {
setCurrentText(translatedText);
}
}, [translatedText, currentText, complete]);
return (
<p className="console-text">
{currentText}
</p>
);
};
You are telling react to do setCurrentText(translatedText) only when it is complete or when the compared text lengths are equal, so yes it continues to write until this moment.
To reset your text when text changes, try creating another useEffect that will reset your states :
useEffect(() => {
index.current = 0;
setCurrentText('');
}, [text]);
Now, I actually did this exact same feature few days ago, here is my component if it can help you :
import React from 'react';
import DOMPurify from 'dompurify';
import './text-writer.scss';
interface ITextWriterState {
writtenText: string,
index: number;
}
const TextWriter = ({ text, speed }: { text: string, speed: number }) => {
const initialState = { writtenText: '', index: 0 };
const sanitizer = DOMPurify.sanitize;
const [state, setState] = React.useState<ITextWriterState>(initialState);
React.useEffect(() => {
if (state.index < text.length - 1) {
const animKey = setInterval(() => {
setState(state => {
if (state.index > text.length - 1) {
clearInterval(animKey);
return { ...state };
}
return {
writtenText: state.writtenText + text[state.index],
index: state.index + 1
};
});
}, speed);
return () => clearInterval(animKey);
}
}, []);
// Reset the state when the text is changed (Language change)
React.useEffect(() => {
if (text.length > 0) {
setState(initialState);
}
}, [text])
return <div className="text-writer-component"><span className="text" dangerouslySetInnerHTML={{ __html: sanitizer(state.writtenText) }} /></div>
}
export default TextWriter;
The translation is made outside of the component so you can pass any kind of text to the component.

React Redux Type Error: Cannot read properties of undefined (reading 'comment')

I'm new to React building a simple app and I get this error when I'm trying to add a comment to a photo, the code is working and the state is changing correctly when I hit enter
I the error in this line
const photo = photos.find((item) => item.id === Number(photo_id));
the photos are defined and the id is defined but I get the photo is undefined
I really appreciate it if anyone could help
here's the code
import { useNavigate, useParams } from 'react-router-dom';
import { useSelector, connect } from 'react-redux'
import Photo from './Photo';
import { useState } from 'react';
import { addComment } from './actions'
const PhotoDetail = ({ addComment }) => {
const {photo_id} = useParams();
const navigate = useNavigate();
const [text, setText] = useState('');
const photos = useSelector((state) => state.photoList.photos);
const photo = photos.find((item) => item.id === Number(photo_id));
console.log('here photo id', photo_id)
console.log('here photo', photo)
console.log('here photos', photos)
const comments = photo['comment'];
const handelKeyDown = (e) => {
if (e.key === "Enter") {
const commentData = {text, photo_id}
addComment(commentData);
// navigate('/' + photo.id);
}
}
return (
<div className="detail">
<div className="photos photoDetail">
<Photo key={photo.id} photo={photo}/>
</div>
<div>
<h2>Comments</h2>
<div>
{ comments.map((comment) => (
<p key={comment.id}>{comment.text}</p>
)) }
</div>
<input type="text" value={text} onChange = {
(e) => setText(e.target.value)
} onKeyDown={
handelKeyDown
}/>
</div>
</div>
);
}
const mapDispatchToProps = dispatch => ({
addComment: commentData => dispatch(addComment(commentData))
})
export default connect(null, mapDispatchToProps) (PhotoDetail);
here's the action
export const addComment = (commentData) => {
console.log('test')
return {
type:"ADDCOMMENT",
payload: commentData
};
};
and here's the Reducer
case "ADDCOMMENT":
const idx = Math.floor(Math.random() * 10000) + 1;
const { text, photo_id } = action.payload;
const newComment = {idx, text}
return { ...state, photos:[state.photos.map((image) =>
image.id === photo_id ? image.comment.push(newComment) && image : image),] }
the console
the console
find will return undefined in case nothing matches the required condition.
So, looks like item.id === Number(photo_id) is probably not resolving to true for any photo in photos.
Then, you are trying to access comment on undefined, that's why there's a TypeError.
In action payload photo_id has string type and in reducer you have added === check but image.id holds number type.
Added, a comment in below code for your better understanding.
case "ADDCOMMENT":
const idx = Math.floor(Math.random() * 10000) + 1;
const { text, photo_id } = action.payload;
const newComment = {idx, text}
return { ...state, photos:[state.photos.map((image) =>
// HERE below condition fails always bcz of strict type check, to photo_id
//- either add Number convertion or in payload itself send string type
image.id === photo_id ? image.comment.push(newComment) && image : image),] }
thank you guys your answers were very helpful but I change the logic
I already have the photo I didn't need to map throw the photos so I just add the comment to the photo and return the state and it works!
case "ADDCOMMENT":
const idx = Math.floor(Math.random() * 10000) + 1;
const { text, photo } = action.payload;
console.log('the photo from reducer', photo)
const newComment = {idx, text};
photo.comment.push(newComment) // adding the comment to the photo
// return { ...state, photos:[state.photos.map((image) =>
// Number(image.id) === Number(photo.id) ? image.comment.push(newComment) && image : image),] }
return state;

use Effect not being called as expected

I am trying to implement a simple file upload drop zone in React:
import { useState, useEffect } from 'react';
import './App.css';
const App = () => {
const [isDropzoneActive, setIsDropzoneActive] = useState(false);
const [files, setFiles] = useState([]);
const [currentChunkIndex, setCurrentChunkIndex] = useState(null);
const handleDragOver = e => {
e.preventDefault();
setIsDropzoneActive(true);
};
const handleDragLeave = e => {
e.preventDefault();
setIsDropzoneActive(false);
};
// Update the files array
const handleDrop = e => {
e.preventDefault();
setIsDropzoneActive(false);
// Just overwrite for this simple example
setFiles(e.dataTransfer.files);
};
// Monitor the files array
useEffect(() => {
if (files.length > 0) {
console.log('got a file');
setCurrentChunkIndex(0);
}
}, [files]);
// Monitor the chunk index
useEffect(() => {
if (currentChunkIndex !== null) {
readAndUploadCurrentChunk();
}
}, [currentChunkIndex]);
const readAndUploadCurrentChunk = () => {
// Implement later
};
return (
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={"dropzone" + (isDropzoneActive ? " active" : "")}
>
{files.length > 0 ? 'Uploading' : 'Drop your files here'}
</div>
);
}
export default App;
However it seems that the effect that monitors [currentChunkIndex] is not being called correctly. I have attempted to drag files into the drop zone, one by one. [files] effect it called correctly each time but the effect on [currentChunkIndex] doesn't get called. What am I doing wrong here?
currentChunkIndex changes from null to 0, you set it only to 0.
useEffect(() => {
if (files.length > 0) {
console.log('got a file');
setCurrentChunkIndex(files.length);
}
}, [files]);

In React, how do you apply componentDidMount for this type of component?

I have a React 16 application. I have a component constructed like so ...
const AddressInput = (props) => {
  const classes = useStyles();
  const { disabled, error, location, onClear, placeholder, setLocation, showMap, value } = props;
useEffect(() => {
console.log("use effect 1 ...");
if (value && placesRef?.current) {
placesRef.current.setVal(value);
if (zoom === 0) {
setZoom(16);
}
}
}, [placesRef, value, disabled, zoom]);
useEffect(() => {
console.log("use effect 2 ...");
if (location) {
if (zoom === 0) {
setZoom(16);
}
if (location.lat !== position[0]) {
setPosition([location.lat, location.lng]);
}
}
}, [location, zoom, position]);
useEffect(() => {
console.log("use effect 3 ...");
console.log("setting " + inputId + " to :" + placeholder);
if (document.getElementById(inputId)) {
document.getElementById(inputId).value = placeholder;
}
}, [])
...
console.log("after use effect:");
console.log(placeholder);
  return (
    <div style={error ? { border: "solid red" } : {}}>
...
    </div>
  );
};
export default AddressInput;
I would like to execute some code after the component has rendered. Normally I could use componentDidMount, but I'm not sure how to apply that when the component is constructed like above.
Since you're using a pure functional component you can use the useEffect hook (as of React v16.8) like this:
import React, { useEffect } from "react";
const AddressInput = (props) => {
const classes = useStyles();
const { disabled, error, location, onClear, placeholder, setLocation, showMap, value } = props;
useEffect(() => {
// your code here
}, []) // empty array acts like `componentDidMount()` in a functional component
return (
<div style={error ? { border: "solid red" } : {}}>
...
</div>
);
};
export default AddressInput;
You can read more about useEffect here:
https://reactjs.org/docs/hooks-effect.html

useEffect triggers function several times with proper dependencies

i've got Tabs component, it has children Tab components. Upon mount it calculates meta data of Tabs and selected Tab. And then sets styles for tab indicator. For some reason function updateIndicatorState triggers several times in useEffect hook every time active tab changes, and it should trigger only once. Can somebody explain me what I'm doing wrong here? If I remove from deps of 2nd useEffect hook function itself and add a value prop as dep. It triggers correctly only once. But as far as I've read docs of react - I should not cheat useEffect dependency array and there are much better solutions to avoid that.
import React, { useRef, useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { defProperty } from 'helpers';
const Tabs = ({ children, value, orientation, onChange }) => {
console.log(value);
const indicatorRef = useRef(null);
const tabsRef = useRef(null);
const childrenWrapperRef = useRef(null);
const valueToIndex = new Map();
const vertical = orientation === 'vertical';
const start = vertical ? 'top' : 'left';
const size = vertical ? 'height' : 'width';
const [mounted, setMounted] = useState(false);
const [indicatorStyle, setIndicatorStyle] = useState({});
const [transition, setTransition] = useState('none');
const getTabsMeta = useCallback(() => {
console.log('getTabsMeta');
const tabsNode = tabsRef.current;
let tabsMeta;
if (tabsNode) {
const rect = tabsNode.getBoundingClientRect();
tabsMeta = {
clientWidth: tabsNode.clientWidth,
scrollLeft: tabsNode.scrollLeft,
scrollTop: tabsNode.scrollTop,
scrollWidth: tabsNode.scrollWidth,
top: rect.top,
bottom: rect.bottom,
left: rect.left,
right: rect.right,
};
}
let tabMeta;
if (tabsNode && value !== false) {
const wrapperChildren = childrenWrapperRef.current.children;
if (wrapperChildren.length > 0) {
const tab = wrapperChildren[valueToIndex.get(value)];
tabMeta = tab ? tab.getBoundingClientRect() : null;
}
}
return {
tabsMeta,
tabMeta,
};
}, [value, valueToIndex]);
const updateIndicatorState = useCallback(() => {
console.log('updateIndicatorState');
let _newIndicatorStyle;
const { tabsMeta, tabMeta } = getTabsMeta();
let startValue;
if (tabMeta && tabsMeta) {
if (vertical) {
startValue = tabMeta.top - tabsMeta.top + tabsMeta.scrollTop;
} else {
startValue = tabMeta.left - tabsMeta.left;
}
}
const newIndicatorStyle =
((_newIndicatorStyle = {}),
defProperty(_newIndicatorStyle, start, startValue),
defProperty(_newIndicatorStyle, size, tabMeta ? tabMeta[size] : 0),
_newIndicatorStyle);
if (isNaN(indicatorStyle[start]) || isNaN(indicatorStyle[size])) {
setIndicatorStyle(newIndicatorStyle);
} else {
const dStart = Math.abs(indicatorStyle[start] - newIndicatorStyle[start]);
const dSize = Math.abs(indicatorStyle[size] - newIndicatorStyle[size]);
if (dStart >= 1 || dSize >= 1) {
setIndicatorStyle(newIndicatorStyle);
if (transition === 'none') {
setTransition(`${[start]} 0.3s ease-in-out`);
}
}
}
}, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);
useEffect(() => {
const timeout = setTimeout(() => {
setMounted(true);
}, 350);
return () => {
clearTimeout(timeout);
};
}, []);
useEffect(() => {
if (mounted) {
console.log('1st call mounted');
updateIndicatorState();
}
}, [mounted, updateIndicatorState]);
let childIndex = 0;
const childrenItems = React.Children.map(children, child => {
const childValue = child.props.value === undefined ? childIndex : child.props.value;
valueToIndex.set(childValue, childIndex);
const selected = childValue === value;
childIndex += 1;
return React.cloneElement(child, {
selected,
indicator: selected && !mounted,
value: childValue,
onChange,
});
});
const styles = {
[size]: `${indicatorStyle[size]}px`,
[start]: `${indicatorStyle[start]}px`,
transition,
};
console.log(styles);
return (
<>
{value !== 2 ? (
<div className={`tabs tabs--${orientation}`} ref={tabsRef}>
<span className="tab__indicator-wrapper">
<span className="tab__indicator" ref={indicatorRef} style={styles} />
</span>
<div className="tabs__wrapper" ref={childrenWrapperRef}>
{childrenItems}
</div>
</div>
) : null}
</>
);
};
Tabs.defaultProps = {
orientation: 'horizontal',
};
Tabs.propTypes = {
children: PropTypes.node.isRequired,
value: PropTypes.number.isRequired,
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
onChange: PropTypes.func.isRequired,
};
export default Tabs;
useEffect(() => {
if (mounted) {
console.log('1st call mounted');
updateIndicatorState();
}
}, [mounted, updateIndicatorState]);
This effect will trigger whenever the value of mounted or updateIndicatorState changes.
const updateIndicatorState = useCallback(() => {
...
}, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);
The value of updateIndicatorState will change if any of the values in its dep array change, namely getTabsMeta.
const getTabsMeta = useCallback(() => {
...
}, [value, valueToIndex]);
The value of getTabsMeta will change whenever value or valueToIndex changes. From what I'm gathering from your code, value is the value of the selected tab, and valueToIndex is a Map that is re-defined on every single render of this component. So I would expect the value of getTabsMeta to be redefined on every render as well, which will result in the useEffect containing updateIndicatorState to run on every render.

Resources