React update list on state change with hooks - reactjs

I am trying to make a photo album component in React which retrieves photo-urls from Firebase Storage and adds them to state with React Hooks. The problem is that the <img/> tags are never created, even though I can see that the image-urls are set correctly as state via the React Chrome Extension.
Complete code of the component:
import React, {useEffect, useState} from 'react';
import {Fab} from "#material-ui/core";
import AddIcon from '#material-ui/icons/Add';
import {colorTheme} from "../constants/colors";
import firebase from 'firebase/app';
import '#firebase/firestore';
import '#firebase/auth';
import '#firebase/storage';
export default function PhotoAlbum() {
const storageRef = firebase.storage().ref();
const [images, setImages] = useState([]);
useEffect(() => {
loadImages();
}, []);
function loadImages() {
let imageUrls = [];
const imagesRef = storageRef.child('/images');
imagesRef.listAll().then(res => {
res.items.forEach(resItem => {
resItem.getDownloadURL().then( url => {
imageUrls.push(url)
})
})
}).then(() => setImages(imageUrls)); // I think this works, I can see the urls on the state
}
function handleChange(e) {
let files = e.target.files;
for(let i = 0; i < files.length; i++){
const file = files[i];
storageRef
.child( `/images/${file.name}` )
.put(file)
.then( () => {
console.log( "Added file to storage! ", file.name );
});
}
}
return (
<div>
{images.map((url, index) => (
<img src={url} key={index.toString()} alt={'this is an image'}/> // These are never rendered
))}
<div style={styles.floatingContainer}>
<input
accept="image/*"
style={styles.input}
id="contained-button-file"
multiple
type="file"
onChange={handleChange}
/>
<label htmlFor={"contained-button-file"}>
<Fab
color='primary'
aria-label='add'
style={styles.floatingButton}
component="span">
<AddIcon/>
</Fab>
</label>
</div>
</div>
)
}
const styles = {
floatingContainer: {
borderRadius: '30px',
position: 'absolute',
right: '2vw',
bottom: '2vh'
},
floatingButton: {
backgroundColor: colorTheme.darkGreen,
},
input: {
display: 'none',
},
};
I am not that familiar with React and I am sure I have just misunderstood something. I apriciate any tips and help!

I actually solved this, the problem was that I didn't update the state when the images were loaded, I just pushed them to an array, so the view never re-rendered. I changed it to this:
function loadImages() {
const imagesRef = storageRef.child('/images');
imagesRef.listAll().then(res => {
res.items.forEach(resItem => {
resItem.getDownloadURL().then(url => {
setImages(oldArray => [...oldArray, url]) // This line has changed!
})
})
});
}
Now it works!

Related

Torch working on Android but not in iOS (ReactJS)

I'm building a QR scanner inside a ReactJS web app that is supposed to run on both Android and iOS. However, I cannot get the torch/flashlight to work on iOS.
I'm using the #blackbox-vision toolbox to handle both the torch and the QR scanner. As far as I understand you need to start the camera functionality and can use the video stream to manipulate the torch. Below code works fine on Android but not on iOS:
import { useState, useEffect, useRef } from "react";
import { QrReader } from "#blackbox-vision/react-qr-reader";
import { useTorchLight } from "#blackbox-vision/use-torch-light";
import styles from "./view.module.css";
import IconButton from "../../components/UI/iconbutton/view";
function SAQRView() {
const streamRef = useRef(null);
const [on, toggle] = useTorchLight(streamRef.current);
const [showTorchToggleButton, setShowTorchToggleButton] = useState(false);
const [msg, setMsg] = useState("");
const setRef = ({ stream }) => {
streamRef.current = stream;
setShowTorchToggleButton(true);
};
const previewStyle = {
width: "100%",
};
const onError = (error) => {
console.log(error);
};
const onTorchClick = (event) => {
toggle();
};
return (
<>
<div className={styles.container}>
<div className={styles.sub_container}>
<QrReader
delay={100}
showViewFinder={false}
style={previewStyle}
onLoad={setRef}
onError={onError}
onScan={setData}
constraints={{
facingMode: "environment",
video: true,
}}
/>
<div className={styles.footer}>
{showTorchToggleButton && (
<IconButton
icon="Flash_off"
toggleIcon="Flash_on"
isToggled={on}
onClick={onTorchClick}
/>
)}
</div>
{msg}
</div>
</div>
</>
);
}
export default SAQRView;
So then I tried manipulating the video stream manually:
import { useState, useEffect, useRef } from "react";
import { QrReader } from "#blackbox-vision/react-qr-reader";
import { useTorchLight } from "#blackbox-vision/use-torch-light";
import styles from "./view.module.css";
import IconButton from "../../components/UI/iconbutton/view";
function SAQRView() {
const streamRef = useRef(null);
const [on, toggle] = useTorchLight(streamRef.current);
const [showTorchToggleButton, setShowTorchToggleButton] = useState(false);
const [msg, setMsg] = useState("");
const setRef = ({ stream }) => {
streamRef.current = stream;
setShowTorchToggleButton(true);
};
const previewStyle = {
width: "100%",
};
const onError = (error) => {
console.log(error);
};
const onTorchClick = (event) => {
const tracks = streamRef.current.getVideoTracks();
const track = tracks[0];
setMsg(JSON.stringify(track.getCapabilities(), null, 2));
try {
if (!track.getCapabilities().torch) {
alert("No torch available.");
}
track.applyConstraints({
advanced: [
{
torch: true,
},
],
});
} catch (error) {
alert(error);
}
};
return (
<>
<div className={styles.container}>
<div className={styles.sub_container}>
<QrReader
delay={100}
showViewFinder={false}
style={previewStyle}
onLoad={setRef}
onError={onError}
onScan={setData}
constraints={{
facingMode: "environment",
video: true,
}}
/>
<div className={styles.footer}>
{showTorchToggleButton && (
<IconButton
icon="Flash_off"
toggleIcon="Flash_on"
isToggled={on}
onClick={onTorchClick}
/>
)}
</div>
{msg}
</div>
</div>
</>
);
}
export default SAQRView;
Again, this works on Android, but not iOS. Notice that I stringify the track capabilities and print them at the bottom of the screen. For Android this looks as follows:
And for iOS, it looks like this:
So it seems that iOS cannot access the torch capability. However, the torch will be greyed out when the QR scanner is active, so it does seem to grab hold of the torch.
Also we have tried installing the Chrome web browser but this gave exactly the same result.
Can I get around this and if so, how?

How do I unit test an object of arrays in React Testing Library?

I have a parent component CafeList.js that makes a service call to firebase and returns cafe data as arrays within an object. I pass this entire object as props to Cafe.js where it is mapped over, and properties are destructured out and rendered (eg, the cafe name: name).
I want to write a test to check that the name element is being rendering in the Cafe.js component, but I'm not sure how to access props that are in the form of an array or object. I'm new to RTL so am a bit lost - any suggestions?
CafeList.jsx
import React, { useState,useEffect } from 'react'
import db from '../fbConfig'
import Cafe from './Cafe'
const CafeList = () => {
const [cafes,setCafe] = useState([])
useEffect(() => {
let cafeArray = []
db.collection('cafes')
.get()
.then(snapshot => {
snapshot.forEach(cafe => {
cafeArray.push(cafe)
})
setCafe(cafeArray)
})
},[])
const [...cafeData] = cafes.map((cafe) => {
const { name, photoURL } = cafe.data()
return { name:name,photoURL:photoURL, id:cafe.id}
})
return(
<div className="cafe-container-container">
<h2 className = 'main-subheading'>Reviews</h2>
<Cafe cafes = {cafeData}/>
</div>
)
}
export default CafeList
Cafe.jsx
import React from 'react'
import {Link} from 'react-router-dom'
const Cafe = ({ cafes }) => {
return (
<div className="cafe-grid">
{
cafes.map((cafe) => {
return (
<Link
to={`/cafe-reviews/${cafe.id}`}
style={{ textDecoration: "none", color: "#686262" }}
>
<div className="cafe-container">
<h3>{cafe.name}</h3>
<img src={cafe.photoURL}></img>
</div>
</Link>
)
})
}
</div>
)
}
export default Cafe
cafe.test.js
import { render, screen } from '#testing-library/react'
import Cafe from '../components/Cafe'
test('is cafe name rendering', () =>{
render(<Cafe cafe = {[]}/>)
const nameElement = screen.getByText(//the cafe name as passed by props)
expect(nameElement).toBeInTheDocument
})

update react state based on incoming activities from botframework v4 in webchat not working?

I'm trying to change state on the web page outside the webchat component but not able to do it.
import { useMemo, useState, useEffect, useRef, useCallback } from 'react';
import ReactWebChat, { createDirectLine, createStore, createStoreWithDevTools, createStyleSet,
createCognitiveServicesSpeechServicesPonyfillFactory, createBrowserWebSpeechPonyfillFactory }
from 'botframework-webchat';
import { GrClose } from "react-icons/gr"
const ChatbotContent = (props) => {
const store =
createStore({}, ({ dispatch }) => next => action => {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
}
else if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
console.log(action.type)
const { activity } = action.payload;
console.log(activity)
if (activity.type === 'message') {
if (activity.replyToId) {
//alert(JSON.stringify(activity, null, 2));
console.log("hola")
setcheck(true)
props.avatarchange()
}
}
}
return next(action);
})
const [check, setcheck] = useState(false)
const styleOptions = {
bubbleBackground: '#FFFFFF',
bubbleFromUserBackground: '#4C12A1',
hideUploadButton: true,
sendBoxBackground: "transparent"
};
const directLine = useMemo(() => createDirectLine({ token: '' }), []);
const speech = useMemo(() => createBrowserWebSpeechPonyfillFactory(), []);
return (
<div style={{ height: "100%", width: "100%" }}>
<div style={{ float: "right" }}>
<GrClose onClick={props.chatchange} />
</div>
<ReactWebChat directLine={directLine} userID="YOUR_USER_ID" store={store} styleOptions={styleOptions} webSpeechPonyfillFactory={speech} />
</div>
)
}
export default ChatbotContent
the console log is working but the following statechange and to trigger the function from props are not working
if (activity.type === 'message') {
if (activity.replyToId) {
console.log("hola")
setcheck(true)
props.avatarchange()
}
How can we change state within the store based on events in webchat ?

How to create infinite scroll in React and Redux?

import React, {useState, useEffect} from 'react';
import {connect} from 'react-redux';
import {
fetchRecipes
} from '../../store/actions';
import './BeerRecipes.css';
const BeerRecipes = ({recipesData, fetchRecipes}) => {
const [page, setPage] = useState(1);
const [recipes, setRecipes] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchRecipes();
}, [])
return (
<div className='beer_recipes_block'>
<div className='title_wrapper'>
<h2 className='title'>Beer recipes</h2>
</div>
<div className='beer_recipes'>
<ul className='beer_recipes_items'>
{
recipesData && recipesData.recipes && recipesData.recipes.map(recipe =>
<li className='beer_recipes_item' id={recipe.id}>{recipe.name}</li>
)
}
</ul>
</div>
</div>
);
};
const mapStateToProps = state => {
return {
recipesData: state.recipes
}
}
const mapDispatchToProps = dispatch => {
return {
fetchRecipes: () => dispatch(fetchRecipes())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(BeerRecipes);
this is my component where I would like to create infinite scroll and below is my redux-action with axios:
import axios from "axios";
import * as actionTypes from "./actionTypes";
export const fetchRecipesRequest = () => {
return {
type: actionTypes.FETCH_RECIPES_REQUEST
}
}
export const fetchRecipesSuccess = recipes => {
return {
type: actionTypes.FETCH_RECIPES_SUCCESS,
payload: recipes
}
}
export const fetchRecipesFailure = error => {
return {
type: actionTypes.FETCH_RECIPES_FAILURE,
payload: error
}
}
export const fetchRecipes = (page) => {
return (dispatch) => {
dispatch(fetchRecipesRequest)
axios
.get('https://api.punkapi.com/v2/beers?page=1')
.then(response => {
const recipes = response.data;
dispatch(fetchRecipesSuccess(recipes));
})
.catch(error => {
const errorMsg = error.message;
dispatch(fetchRecipesFailure(errorMsg));
})
}
}
I want to create a scroll. I need, firstly, to display first 10 elements and then to add 5 elements with every loading. I have 25 elements altogether and when the list is done it should start from the first five again.
Assuming you already have everything ready to load your next page. You can probably simplify the entire process by using a package like react-in-viewport so you don't have to deal with all the scroll listeners.
then you use it like this way.
import handleViewport from 'react-in-viewport';
const Block = (props: { inViewport: boolean }) => {
const { inViewport, forwardedRef } = props;
const color = inViewport ? '#217ac0' : '#ff9800';
const text = inViewport ? 'In viewport' : 'Not in viewport';
return (
<div className="viewport-block" ref={forwardedRef}>
<h3>{ text }</h3>
<div style={{ width: '400px', height: '300px', background: color }} />
</div>
);
};
const ViewportBlock = handleViewport(Block, /** options: {}, config: {} **/);
const Component = (props) => (
<div>
<div style={{ height: '100vh' }}>
<h2>Scroll down to make component in viewport</h2>
</div>
<ViewportBlock
onEnterViewport={() => console.log('This is the bottom of the content, lets dispatch to load more post ')}
onLeaveViewport={() => console.log('We can choose not to use this.')} />
</div>
))
What happen here is, it creates a 'div' which is outside the viewport, once it comes into the view port ( it means user already scrolled to the bottom ), you can call a function to load more post.
To Note: Remember to add some kind of throttle to your fetch function.

How can I convert my state from an object into a mappable array?

My "Card Dealer" in CodeSandbox
I was watching a coding challenge done in React with class based components that extracts data from a "card deck API" using Axios. Once you have the deck and the deck ID, you can then render a separate "Card" component that displays the current card and all previous cards.
Rather than use classes, I decided to try this challenge using hooks instead. Due to poor planning, my state is pretty much one big object, and I can't map over it to display multiple "Card" components. I only get the current stateless "child"card displayed at the moment.
How can I change the way I have my state structured so I can map over it to display the "image" prop for all the cards that were previously dealt to display simultaneously? Thanks for any input!
I wanted to be able to do something like:
let card = deck.map((c) => {
return (
<Card image={c.img} value={c.value} />
)
});
//My current code:
import React, { useState, useEffect } from "react";
import Card from "./Card";
import axios from "axios";
function CardDeck() {
let [deck, setDeck] = useState([]);
useEffect(() => {
let shuffle = async () => {
let response = await axios.get(
"https://deckofcardsapi.com/api/deck/new/shuffle"
);
let data = response.data;
setDeck({
id: data.deck_id,
remaining: data.remaining,
shuffled: data.shuffled
});
};
shuffle();
}, []);
let getCard = async () => {
const url = `https://deckofcardsapi.com/api/deck/${deck.id}/draw/`;
let response = await axios.get(url);
let card = response.data.cards;
if (response.data.success) {
setDeck((prev) => {
console.log(deck.drawn);
return {
...prev,
img: card[0].image,
remaining: response.data.remaining,
name: `${card[0].value} of ${card[0].suit}`,
code: card[0].code,
};
});
}
};
return (
<div>
<h1>Card Dealer</h1>
<button onClick={getCard}>Get Card!</button>
<h3>{deck.name}</h3>
<Card
deckID={deck.id}
remaining={deck.remaining}
drawn={deck.drawn}
image={deck.img}
name={deck.name}
id={deck.code}
/>
{deck.remaining === 0 && <button>New Deck</button>}
</div>
)
}
export default CardDeck;
Output:
Here I am storing all the fetched cards in card and then iterating it to show them in a row.
Hope this gives you an idea.
import React, { useState, useEffect } from "react";
import Card from "./Card";
import axios from "axios";
function CardDeck() {
let [cardDeck, setCardDeck] = useState({});
const [card, setCard] = useState([]);
useEffect(() => {
let shuffle = async () => {
let response = await axios.get(
"https://deckofcardsapi.com/api/deck/new/shuffle"
);
let data = response.data;
// console.log(data);
setCardDeck({
id: data.deck_id,
remaining: data.remaining,
shuffled: data.shuffled
});
};
shuffle();
}, []);
let getCard = async () => {
const url = `https://deckofcardsapi.com/api/deck/${cardDeck.id}/draw/`;
let response = await axios.get(url);
let newCard = response.data.cards;
// console.log(newCard);
let temp = [...card];
temp.push(newCard);
console.log(card);
if (cardDeck.remaining > 0) {
setCard(temp);
}
console.log(cardDeck);
};
return !cardDeck.remaining ? (
<div>
<h1>Game Over! Out of cards...</h1>
</div>
) : (
<div>
<h1>Card Dealer</h1>
<button onClick={getCard}>Get Card!</button>
<div
style={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
width: "100%"
}}
>
{card.map((c) => (
<img key={c[0]["image"]} src={c[0]["image"]} alt="card" width={60} />
))}
</div>
</div>
);
}
export default CardDeck;
Working Code: Codesandbox

Resources