Value in countUp and even in typography not updating - reactjs

I'm trying to modify the proyect of { https://www.youtube.com/watch?v=khJlrj3Y6Ls&t=3311s } to my country, so, instead of having countries displayed in the option box and selecting one for updating the value of the cards, I'm trying to achieve to update the numbers when selecting a region, one by one... The options and region names are done.
But, when selecting a region the number is not updating and just displays zero, even is receiving the correct data in JSON (and even passing a number it just desplays zero).
I'm sorry if I'm a bit ambiguous but I'm newbie in React. How I can achieve this?, it just displays zero... And I have a lot of hours trying to resolve this.
Thanks and also, the 99% of the work is from the tutorial of JavaScript Mastery.
There's the code.
import axios from 'axios';
const url = 'https://covid19.mathdro.id/api/countries/CHILE';
export const fetchData = async (provincia) => {
let changeableUrl = url;
if (provincia) {
changeableUrl = `${url}/confirmed`;
try {
const {
data: [
confirmed,
recovered,
deaths,
lastUpdate,
]
} = await axios.get(changeableUrl);
return {
confirmed,
recovered,
deaths,
lastUpdate,
};
} catch (error) {
return error;
}
} else {
try {
const {
data: {
confirmed,
recovered,
deaths,
lastUpdate,
}
} = await axios.get(changeableUrl);
return {
confirmed,
recovered,
deaths,
lastUpdate,
};
} catch (error) {
return error;
}
}
};
export const fetchDailyData = async () => {
try {
const {
data
} = await axios.get(`${url}`);
return data.map(({
confirmed,
deaths,
reportDate: date
}) => ({
confirmed: confirmed.total,
deaths: deaths.total,
date
}));
} catch (error) {
return error;
}
};
export const fetchCountries = async () => {
try {
const {data: provincias } = await axios.get(`${url}/confirmed`);
return provincias.map((provincia) => provincia.provinceState);
} catch (error) {
return error;
}
};
There's the APP.js
import React from 'react';
// import Cards from './components/Cards/Cards.jsx';
// import Chart from './components/Chart/Chart.jsx';
// import CountryPicker from './components/CountryPicker/CountryPicker.jsx';
import {Cards, Chart, CountryPicker} from './components';
import styles from './App.module.css';
import { fetchData} from './api';
// Acá se agrega el componente
class App extends React.Component {
state = {
data: {},
provincia: '',
}
async componentDidMount(){
const fetchedData = await fetchData();
this.setState({ data: fetchedData })
}
handleCountryChange = async (provincia) => {
const fetchedData = await fetchData(provincia);
console.log(fetchedData)
this.setState({ data: fetchedData, provincia: provincia })
}
render() {
const { data, provincia} = this.state;
return (
<div className={styles.container}>
<Cards data={data} />
<CountryPicker handleCountryChange = {
this.handleCountryChange
}
/>
<Chart data={data} provincia={provincia} />
</div>
)
}
}
export default App;
CARDS.js
import React from 'react';
import { Card, CardContent, Typography, Grid } from '#material-ui/core';
import CountUp from 'react-countup';
import cx from 'classnames';
import styles from './Cards.module.css';
const Cards = ({ data :{confirmed, recovered, deaths, lastUpdate}}) => {
if(!confirmed) {
return 'Cargando...';
}
return(
<div className={styles.container}>
<Grid container spacing={3} justify="center">
<Grid item component={Card} xs={12} md={3} className={cx(styles.card, styles.infectados)}>
<CardContent>
<Typography color="textSecondary" gutterBottom>Casos Totales</Typography>
<Typography variant="h5">
<CountUp start={0} end={confirmed.value} duration={2.5} separator=","
/>
</Typography>
<Typography color="TextSecondary">{new Date(lastUpdate).toLocaleDateString()}</Typography>
<Typography variant="body2">Número de Casos Totales de COVID-19</Typography>
</CardContent>
</Grid>
<Grid item component={Card} xs={12} md={3} className={cx(styles.card, styles.recuperados)}>
<CardContent>
<Typography color="textSecondary" gutterBottom>Recuperados</Typography>
<Typography variant="h5">
<CountUp start={0} end={recovered.value} duration={2.5} separator=","
/>
</Typography>
<Typography color="TextSecondary">{new Date(lastUpdate).toLocaleDateString()}</Typography>
<Typography variant="body2">Número de Casos Recuperados Totales de COVID-19</Typography>
</CardContent>
</Grid>
<Grid item component={Card} xs={12} md={3} className={cx(styles.card, styles.muertes)}>
<CardContent>
<Typography color="textSecondary" gutterBottom>Muertes</Typography>
<Typography variant="h5">
<CountUp start={0} end={deaths.value} duration={2.5} separator=","
/>
</Typography>
<Typography color="TextSecondary">{new Date(lastUpdate).toLocaleDateString()}</Typography>
<Typography variant="body2">Número de muertes totales causadas por COVID-19</Typography>
</CardContent>
</Grid>
</Grid>
</div>
)
}
export default Cards;
And the COUNTRYPICKER.js (regionpicker in my case)
import React, { useState, useEffect} from 'react';
import { NativeSelect, FormControl } from '#material-ui/core';
import styles from './CountryPicker.module.css';
import { fetchCountries } from '../../api';
const CountryPicker = ({
handleCountryChange
}) => {
const [fetchedCountries, setFetchedCountries] = useState([]);
useEffect(() => {
const fetchAPI = async () => {
setFetchedCountries(await fetchCountries());
}
fetchAPI();
}, [setFetchedCountries]);
return (
<FormControl className={styles.formControl}>
<NativeSelect defaultValue = ""
onChange = {
(e) => {
handleCountryChange(e.target.value)
}
} >
{/* Lo siguiente es para el selector de países */}
<option value="Chile">Todo Chile</option>
{fetchedCountries.map((provincia, i) => <option key={i} value={provincia}>{provincia}</option>)}
</NativeSelect>
</FormControl>
)
}
export default CountryPicker;
At first it is updating correct
JSON when it is updating at correct
Then, when selecting a region it is not updating and just displays zero
JSON when it is not displaying correctly

the problem is the API response format for the provincia data.
I fixed it doing this in the api/index.js
export const fetchData = async (provincia) => {
let changeableUrl = url;
if (provincia) {
changeableUrl = `${url}/confirmed`;
try {
const data = await axios.get(changeableUrl);
const filteredData = data.data.find((province) => {
if (province.provinceState == provincia){
return province;
};
})
return {confirmed: {value: filteredData.confirmed} , recovered: {value: filteredData.recovered} , deaths: {value: filteredData.deaths},
lastUpdate: new Date(filteredData.lastUpdate).toISOString()};
} catch (error) {
return error;
}
} else {
try {
const {
data: {
confirmed,
recovered,
deaths,
lastUpdate,
}
} = await axios.get(changeableUrl);
console.log("fetchdata else: ", confirmed,
recovered,
deaths,
lastUpdate);
return {
confirmed,
recovered,
deaths,
lastUpdate,
};
} catch (error) {
return error;
}
}
};
Let me know if that works for you.

Related

How to query data using 'useQueries' when it is dependent on previous query?

I am creating a webapp where I can show a list of Pokemons on initial page load using PokéAPI. The list should show all the images of the Pokemons and its name, inside of an MUI Card, something like this: App UI example.
I am using two APIs. One for getting the list of all the Pokemon names and their details. Another for getting the specific Pokemon's image.
In order to create the UI, I am querying the first API to get the list of all the Pokemons. And then I am using the result of the first API in the second query to get the image of that Pokemon.
Here are both the components that I have created:
PokemonList.jsx
import Box from "#mui/material/Box";
import { useQueries, useQuery } from "#tanstack/react-query";
import axios from "axios";
import PokemonCard from "./PokemonCard";
const getPokemonList = async () => {
const { data } = await axios.get(
"https://pokeapi.co/api/v2/pokemon?limit=12&offset=0"
);
console.log(data);
return data;
};
const usePokemonList = () => {
return useQuery(["pokemonList"], () => getPokemonList());
};
const getPokemons = async (url) => {
const { data } = await axios.get(url);
return data;
};
const usePokemons = (pokemons) => {
return useQueries({
queries: pokemons?.results.map((pokemon) => {
return {
queryKey: ["pokemons", pokemon.name],
queryFn: getPokemons.bind(this, pokemon.url),
staleTime: Infinity,
enabled: !!pokemons.count,
};
}),
});
};
function PokemonList() {
const { data: pokemonList } = usePokemonList();
const { data: pokemons, isLoading } = usePokemons(pokemonList);
return (
<Box
sx={{
display: "flex",
justifyContent: "space-evenly",
flexFlow: "row wrap",
gap: "2em",
}}
>
{!isLoading &&
pokemons.map((pokemon) => (
<PokemonCard
key={pokemon.species.name}
name={pokemon.species.name}
image={pokemon.sprites.front_default}
/>
))}
</Box>
);
}
export default PokemonList;
PokemonCard.jsx
import { Card, CardContent, CardMedia, Typography } from "#mui/material";
import PropTypes from "prop-types";
PokemonCard.propTypes = {
image: PropTypes.string,
name: PropTypes.string,
};
function PokemonCard(props) {
return (
<Card sx={{ width: 225, flexWrap: "wrap" }}>
<CardMedia component="img" height="140" image={props.image} />
<CardContent>
<Typography variant="caption">{props.name}</Typography>
</CardContent>
</Card>
);
}
export default PokemonCard;
This is the error that I am getting, because the first API has not resolved yet. What should be the proper way to use useQueries here?
The error is telling you that the thing you're trying to map for your useQueries call is null or undefined. Which makes sense if the previous result has not yet returned. Additionally your hooks are a little too brittle in how their integrated. I would suggest refactoring your usePokemons hook to just take a list of pokemons (not an object with results) and default it to being empty. e.g.
const usePokemons = (pokemons) => {
pokemons = pokemons || [];
return useQueries({
queries: pokemons.map((pokemon) => {
return {
queryKey: ["pokemons", pokemon.name],
queryFn: getPokemons.bind(this, pokemon.url),
staleTime: Infinity,
};
}),
});
};
Then you just change how you integrate the two:
function PokemonList() {
const { data: pokemonList, isLoading: isListLoading } = usePokemonList();
const { data: pokemons, isLoading: isImagesLoading } = usePokemons(pokemonList?.results);
// ... etc

React REDUX is updating all state after the update action

I've been figuring out this bug since yesterday.
All of the states are working before the update action. I have console log all the states before the update action.
Then after creating a model, the update action is executed.
This is the result when I console log.
I wondered why dataGrid returns an error since I point to all the id in the DataGrid component.
Uncaught Error: MUI: The data grid component requires all rows to have a unique `id` property.
This is my code:
Models Reducer:
import * as actionTypes from 'constants/actionTypes';
export default (models = [], action) => {
switch (action.type) {
case actionTypes.FETCH_MODELS:
return action.payload.result;
case actionTypes.CREATE:
return [...models, action.payload.result];
case actionTypes.UPDATE:
return models.map((model) => (model.model_id === action.payload.result.model_id ? action.payload.result : model));
case actionTypes.DELETE:
return models.filter((model) => model.model_id !== action.payload);
default:
return models;
}
};
In my model component:
import * as actionTypes from 'constants/actionTypes';
export default (models = [], action) => {
switch (action.type) {
case actionTypes.FETCH_MODELS:
return action.payload.result;
case actionTypes.CREATE:
return [...models, action.payload.result];
case actionTypes.UPDATE:
return models.map((model) => (model.model_id === action.payload.result.model_id ? action.payload.result : model));
case actionTypes.DELETE:
return models.filter((model) => model.model_id !== action.payload);
default:
return models;
}
};
My ModelForm:
<Formik
enableReinitialize={true}
initialValues={modelData}
validationSchema={Yup.object().shape({
model_code: Yup.string(4).min(4, 'Minimum value is 4.').max(50, 'Maximum value is 4.').required('Model code is required'),
model_description: Yup.string().max(200, 'Maximum value is 200.'),
model_status: Yup.string().min(5).max(10, 'Maximum value is 10.')
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
if (scriptedRef.current) {
if (currentId === 0) {
// , name: user?.result?.name
dispatch(createModel({ ...values }, setFormVisible));
} else {
dispatch(updateModel(currentId, { ...values }, setFormVisible));
}
setStatus({ success: true });
setSubmitting(false);
}
} catch (err) {
console.error(err);
if (scriptedRef.current) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}
}}
>
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, resetForm, values }) => (
<form noValidate onSubmit={handleSubmit}>
<Grid container spacing={1}>
<Grid item lg={4} md={4} sm={12}>
<JTextField
label="Model"
name="model_code"
value={values.model_code}
onBlur={handleBlur}
onChange={handleChange}
touched={touched}
errors={errors}
/>
</Grid>
</Grid>
<Grid container spacing={1} sx={{ mt: 1 }}>
<Grid item lg={4} md={4} sm={12}>
<JTextField
label="Description"
name="model_description"
value={values.model_description}
onBlur={handleBlur}
onChange={handleChange}
touched={touched}
type="multiline"
rows={4}
errors={errors}
/>
</Grid>
</Grid>
{currentId ? (
<Grid container spacing={1} sx={{ mt: 1 }}>
<Grid item lg={4} md={4} sm={12}>
<JSelect
labelId="model_status"
id="model_status"
name="model_status"
value={values.model_status}
label="Status"
onBlur={handleBlur}
onChange={handleChange}
errors={errors}
>
<MenuItem value="ACTIVE">ACTIVE</MenuItem>
<MenuItem value="INACTIVE">INACTIVE</MenuItem>
</JSelect>
</Grid>
</Grid>
) : (
''
)}
<Box sx={{ mt: 2 }}>
<ButtonGroup variant="contained" aria-label="outlined button group">
<Button size="small" disabled={isSubmitting} type="submit">
Save
</Button>
<Button size="small" onClick={resetForm}>
Cancel
</Button>
{currentId ? (
<Button size="small" color="secondary" onClick={handleDelete}>
Delete
</Button>
) : (
''
)}
</ButtonGroup>
</Box>
</form>
)}
</Formik>
Why products, parts or other states are updating too? Since I only update the model create action?
Please check this out: https://www.awesomescreenshot.com/video/11412230?key=a0212021c59aa1097fa9d38917399fe3
I Hope someone could help me figure out this bug. This is only the problem else my CRUD template is good.
Update:
Found out that actions in redux should always be unique or else multiple reducers with the same action name will be triggered.
I have updated my action types to:
// AUTHENTICATION ACTIONS
export const AUTH = 'AUTH';
export const LOGOUT = 'LOGOUT';
// MODEL ACTIONS
export const FETCH_MODELS = 'FETCH_MODELS';
export const CREATE_MODEL = 'CREATE_MODEL';
export const UPDATE_MODEL = 'UPDATE_MODEL';
export const DELETE_MODEL = 'DELETE_MODEL';
// PRODUCTS ACTIONS
export const FETCH_PRODUCTS = 'FETCH_PRODUCTS';
export const CREATE_PRODUCT = 'CREATE_PRODUCT';
export const UPDATE_PRODUCT = 'UPDATE_PRODUCT';
export const DELETE_PRODUCT = 'DELETE_PRODUCT';
// ASSEMBLY ACTIONS
export const FETCH_ASSEMBLY = 'FETCH_ASSEMBLY';
export const CREATE_ASSEMBLY = 'CREATE_ASSEMBLY';
export const UPDATE_ASSEMBLY = 'UPDATE_ASSEMBLY';
export const DELETE_ASSEMBLY = 'DELETE_ASSEMBLY';
// PARTS ACTIONS
export const FETCH_PARTS = 'FETCH_PARTS';
export const CREATE_PART = 'CREATE_PART';
export const UPDATE_PART = 'UPDATE_PART';
export const DELETE_PART = 'DELETE_PART';
Reducers to:
import * as actionTypes from 'constants/actionTypes';
export default (models = [], action) => {
switch (action.type) {
case actionTypes.FETCH_MODELS:
return action.payload.result;
case actionTypes.CREATE_MODEL:
return [...models, action.payload.result];
case actionTypes.UPDATE_MODEL:
console.log(models);
return models.map((model) => (model.model_id === action.payload.result.model_id ? action.payload.result : model));
case actionTypes.DELETE_MODEL:
return models.filter((model) => model.model_id !== action.payload);
default:
return models;
}
};
and Actions to:
import * as actionTypes from 'constants/actionTypes';
import * as api from 'api/index.js';
import Swal from 'sweetalert2';
export const getModels = () => async (dispatch) => {
try {
const { data } = await api.fetchModels();
dispatch({ type: actionTypes.FETCH_MODELS, payload: data });
} catch (error) {
console.log(error);
Swal.fire('Error!', 'Something went wrong', 'error');
}
};
export const createModel = (model, setFormVisible) => async (dispatch) => {
try {
const { data } = await api.createModel(model);
dispatch({ type: actionTypes.CREATE_MODEL, payload: data });
setFormVisible(false);
Swal.fire('Success!', 'Model has been added successfully', 'success');
} catch (error) {
console.log(error);
Swal.fire('Error!', 'Something went wrong', 'error');
}
};
export const updateModel = (id, model, setFormVisible) => async (dispatch) => {
try {
const { data } = await api.updateModel(id, model);
dispatch({ type: actionTypes.UPDATE_MODEL, payload: data });
setFormVisible(false);
Swal.fire('Success!', 'Model updated successfully', 'success');
} catch (error) {
console.log(error);
Swal.fire('Error!', 'Something went wrong', 'error');
}
};
export const deleteModel = (id) => async (dispatch) => {
try {
await await api.deleteModel(id);
dispatch({ type: actionTypes.DELETE_MODEL, payload: id });
Swal.fire('Success!', 'Model deleted successfully', 'success');
} catch (error) {
console.log(error);
Swal.fire('Error!', 'Something went wrong', 'error');
}
};

Uncaught (in promise) TypeError: Cannot read property 'httpsCallable' of undefined (Firebase functions issue)

I'm going through the docs, and I cannot see where I'm going wrong. Can somebody maybe shed some light? I think I'm not referencing functions() or firebase correctly. Thanks! Here's all of the code:
firebase.js
import firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/firestore'
import 'firebase/functions'
const dev = true
export const app = !firebase.apps.length ? firebase.initializeApp({
apiKey: "AIzaSyBDrj-rL-Mzswu9VXhgp-RuvP9Hvl1kqQ0",
authDomain: "edit-elements.firebaseapp.com",
databaseURL: "https://edit-elements-default-rtdb.firebaseio.com",
projectId: "edit-elements",
storageBucket: "edit-elements.appspot.com",
messagingSenderId: "340652433701",
appId: "1:340652433701:web:a26472592c1538bbac7acc",
measurementId: "G-945XC7348K"
}) : firebase.app()
export const auth = app.auth()
export const db = app.firestore()
export const functions = dev ? app.functions().useEmulator("localhost", '5001') : app.functions()
checkout.js (where the function createFreeOrder is called)
import PayPalComponent from '../components/PayPalComponent'
import { Button, Grid, TextField, Typography } from '#material-ui/core'
import { makeStyles } from '#material-ui/core/styles'
import { useAuth } from '../contexts/AuthContext'
import { useCart } from '../contexts/CartContext'
import Cart from '../components/Cart'
import LoginForm from '../components/LoginForm'
import SignupForm from '../components/SignupForm'
import { useEffect, useState } from 'react'
import { client } from '../prismic-configuration'
import { Actions } from '../utils/cartActions'
import { db, functions } from '../firebase'
const theme = makeStyles({
checkoutContainer: {
margin: 'auto',
maxWidth: '1200px',
paddingTop: '2rem',
'&: section': {
minHeight: '100vh'
},
couponInput: {
'& fieldset': {
borderRadius: '6px 0px 0px 6px'
}
}
}
})
export default function Checkout() {
const { currentUser } = useAuth()
const { cart, cartTotal, dispatch } = useCart()
const styles = theme()
const [showLogin, setShowLogin] = useState(false)
const hasItems = Boolean(Object.keys(cart.products).length)
const [couponCode, setCouponCode] = useState('')
const [couponError, setCouponError] = useState('')
const cartCost = cartTotal()
console.log(functions, db)
const checkCoupon = async () => {
setCouponError('')
if (couponCode == '') return null
console.log('NOTICE: Please note that the coupons are checked server-side, so any attempt to manipulate them here will do nothing, and you *will* be charged the price without a valid coupon')
await client.getByUID('coupon', couponCode.toLowerCase())
.then(res => {
if (res.data) dispatch({ type: Actions.ADD_COUPON, payload: res })
})
.catch(err => setCouponError('No Coupon / Expired Coupon'))
}
const handleFreeOrder = async () => {
const createFreeOrder = functions.httpsCallable('createFreeOrder')
createFreeOrder(cart.products)
}
return (
<Grid container direction="column" className={styles.checkoutContainer}>
<Grid item>
<Typography variant="h1" mb={'2rem'}>Shopping Cart</Typography>
</Grid>
{ !currentUser &&
<>
<Grid item p={'.5rem'}>
<Typography variant="subtitle1">Please note that accounts are required for purchase, as it allows us to securely generate download tokens and process invoices.</Typography>
</Grid>
{ !showLogin ?
<Grid item>
<SignupForm dontRedirect/>
<Typography>Already have an account? <span style={{cursor: 'pointer'}} onClick={prev => setShowLogin(true)}>Click here</span> to login.</Typography>
</Grid>
:
<Grid item>
<LoginForm />
<Typography>Don't have an account? <span style={{color: '#5e5e5e', cursor: 'pointer'}} onClick={prev => setShowLogin(false)}>Click here</span> to login.</Typography>
</Grid>
}
</>
}
<Grid container>
<Grid item xs={12}>
<Cart />
</Grid>
{ hasItems &&
<Grid item xs={12} sx={{textAlign: 'right'}} p={3} >
<TextField className={styles.couponInput} helperText={couponError} label="Coupon Code" value={couponCode.toUpperCase()} onChange={(e) => setCouponCode(e.target.value)} /><Button disableElevation sx={{padding: '1rem', borderRadius: '0px 6px 6px 0px'}} onClick={ checkCoupon } variant="contained">Check Code</Button>
</Grid>
}
{ hasItems &&
<Grid container style={{textAlign: 'right'}} justifyContent="flex-end">
<Grid item xs={12} sm={8} p={3}>
{ (cartTotal() > 0.00) ? <PayPalComponent /> : <Button onClick={handleFreeOrder} color="amber" style={{ padding: '1rem' }} variant="contained">Claim Product(s)</Button>}
</Grid>
</Grid>
}
</Grid>
</Grid>
)
}
functions/index.js file (firebase functions file):
const firebase = require('firebase-admin');
const functions = require('firebase-functions');
// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions
//
// exports.helloWorld = functions.https.onRequest((request, response) => {
// functions.logger.info("Hello logs!", {structuredData: true});
// response.send("Hello from Firebase!");
// });
exports.newsletterSignup = functions.https.onCall((data, ctx) => {
console.log('ctx obj', ctx, 'ctx req', ctx.req)
const sgMail = require('#sendgrid/mail')
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
const msg = ("hello")
res.send('Received').end()
})
exports.createFreeOrder = functions.https.onCall(async (data, ctx) => {
const firebase = require('firebase-admin')
const db = require('firebase/firebase-firestore')
console.log('data and ctx', data, ctx)
})
exports.createPaypalOrder = functions.https.onCall(async (data, ctx) => {
const checkoutNodeSDK = require('#paypal/checkout-server-sdk')
const Prismic = require('#prismicio/client')
const products = data.products
const env = () => {
const clientId = process.env.PAYPAL_CLIENT.id
const clientSecret = process.env.PAYPAL_CLIENT.clientSecret
return new checkoutNodeSDK.core.SandboxEnvironment(clientId, clientSecret)
}
const client = () => {
return new checkoutNodeSDK.core.PayPalHttpClient(env)
}
// The request for PayPal
const request = new paypal.orders.OrdersCreateRequest()
request.prefer('return=representation')
request.requestBody({
intent: 'Capture',
purchase_units: [{
amount: {
currency_code: 'USD',
value: 0
}
}]
})
let order
try {
order = await client().execute(request)
} catch {
}
})
The problem is here:
export const functions = dev ? app.functions().useEmulator("localhost", '5001') : app.functions()
httpsCallable does not exists on the useEmulator method. Instead you should specify to use emulators before trying to use them like this:
const dev = true;
const functions = app.functions();
// Checking if dev is true. If yes, use emulator
if (dev) functions.useEmulator("localhost", 5001)
export {functions}

Redux doesn't fetch array after action has dispatched

I'm looking to append the data in the UPLOAD_IMAGE to GET_IMAGES. Without having to re-rerendering the component. Or in other words, without having to refresh the page.
I get type errors whenever img is followed
<Typography className={classes.imageTypographyTitle} variant="h4" align="center">{img.image_title}</Typography>
<Divider className={classes.imageDivider} variant="middle" />
<Image image_url={img.img_url} />
<Typography variant="h6" align="center">{img.user.username}</Typography>
<Typography variant="h6" align="center">{moment(img.created_at).calendar()}</Typography>
........
TypeError: Cannot read property 'image_title' of undefined
On refresh i see the new data, and i can add data, and i can see the updated array. The type error only happens if the images array is empty.
I would like to append the data to the empty array, and show the data without re render/refresh or any type errors errors.
Should i use another lifecycle method ? because componentWillMount Cannot be called twice, just once. So given that array is empty, should i use something like shouldComponentUpdate to fetch the initial data ?
data structure given that their is existing data in the array.
0:{
"id": 71,
"image_title": "ii",
"img_url": "https://*********",
"created_at": "2019-06-24T02:36:48.359Z",
"updated_at": "2019-06-24T02:36:48.359Z",
"user_id": 1,
"user": {
"id": 1,
"googleId": null,
"username": "a******",
"password": "**********",
"email": "a********",
"created_at": "2019-06-23T18:57:17.253Z",
"updated_at": "2019-06-23T18:57:17.253Z"
},
"comments": []
}
reducer
import { GET_IMAGES, POST_COMMENT, DELETE_IMAGE, UPLOAD_IMAGE } from '../actions/types';
const initialState = {
images:[],
}
export default (state = initialState, action) => {
switch (action.type) {
case GET_IMAGES:
console.log(action.data);
return{
...state,
images:action.data
}
case UPLOAD_IMAGE:
const newState = {...state}
const myImages = newState.images
// console.log(myImages); // empty array
const newImage = action.newImage
console.log(newImage[0]); // gets the new uploaded image.
return {
images:[
{
id: newImage[0].id,
user:{
username:newImage[0].user.username
},
comments:{
comment_body: newImage[0].comments.comment_body
},
image_title: newImage[0].image_title,
img_url: newImage[0].img_url,
},
myImages[0] // pass the previous images if array
/// isn't empty
]
}
default:
return state;
}
}
action
// upload image
export const uploadImage = data => {
return (dispatch) => {
Axios.post('/images/upload', data).then((response) => {
const newImage = {...response.data}
console.log(newImage);
dispatch({type:UPLOAD_IMAGE, newImage})
// history.push("/dashboard");
});
}
}
// get images
export const getImages = () => {
return async (dispatch) => {
const url = await Axios.get('/images/uploads')
const data = url.data;
dispatch({
type: GET_IMAGES,
data
})
}
}
Dashboard.js
import React, { Component } from "react";
import Button from '#material-ui/core/Button';
import TextField from '#material-ui/core/TextField';
import Grid from '#material-ui/core/Grid';
import Typography from '#material-ui/core/Typography';
import ImageUploader from 'react-images-upload';
import ImageContainer from "./ImageContainer"
import {connect} from 'react-redux';
import {getImages, deleteImage, uploadImage} from '../actions/imageActions';
import dashboardStyles from '../styles/dashboardStyles';
import {withStyles} from '#material-ui/core/styles';
import {compose} from 'redux';
class Dashboard extends Component{
constructor(props){
super(props);
this.state = {
image_url: '',
description:'',
upload:false,
isComment:false,
comment_body:''
}
}
handleUpload = file => {
const data = new FormData()
const image = file[0]
// console.log(this.state.description)
// data.append('ourImage', this.state.description)
data.append('ourImage',image, this.state.description )
this.props.uploadImage(data);
this.setState({
description: ''
})
}
handleChange = (e) => {
// e.preventDefault();
this.setState({
[e.target.name]: e.target.value
})
// console.log(this.state.description)
}
componentDidMount(){
this.props.getImages();
console.log(this.props.image.images);
}
.........
{image.images.length > 0 ? (
image.images.map( (img, i) => (
<div key={i}>
<ImageContainer img={img} deleteImg={() => this.deleteImg(img.id)}/>
</div>
))
) : (
<div>
<Grid item md={8}>
<Typography>No Images yet</Typography>
</Grid>
</div>
)}
const mapStateToProps = (state) => ({
image: state.image
})
const mapDispatchToProps = (dispatch) => ({
getImages: () => dispatch(getImages()),
uploadImage: (data) => dispatch(uploadImage(data))
})
export default compose(connect(mapStateToProps, mapDispatchToProps), withStyles(dashboardStyles))(Dashboard)
image container
render(){
const { img, deleteImg, classes } = this.props
return(
<Grid item sm={12} md={12} className={classes.imageGridItem}>
<Paper className={classes.imageContainerPaper}>
{/* // empty image_title */}
<Typography className={classes.imageTypographyTitle} variant="h4" align="center">{img.image_title}</Typography>
<Divider className={classes.imageDivider} variant="middle" />
<Image image_url={img.img_url} />
<Typography variant="h6" align="center">{img.user.username}</Typography>
<Typography variant="h6" align="center">{moment(img.created_at).calendar()}</Typography>
........
</Grid>
)
}
}
You need to spread the existing images array inside your new state.
case UPLOAD_IMAGE:
const newState = {...state}
const myImages = newState.images
// console.log(myImages); // empty array
const newImage = action.newImage
console.log(newImage[0]); // gets the new uploaded image.
return {
images:[
{
id: newImage[0].id,
user:{
username:newImage[0].user.username
},
comments:{
comment_body: newImage[0].comments.comment_body
},
image_title: newImage[0].image_title,
img_url: newImage[0].img_url,
},
...state.images
]
}
So with that you have a new state, with your new image first, followed by the initial images.
Fix. remove this line.
myImages[0] // pass

Optimistic React Apollo ui lag with React Beautiful Drag and Drop

I'm trying to create a optimistic response where the ui updates immmediately (minimal lag and better user experience) with drag and dropped data. The issue i'm having however is that it lags anyways.
So whats happening is that I expect a list of zones and unassigned zones from my query, unassignedZone is a object, with cities in them, and zones is a list of zones with cities within them. When writing my mutation, I return the new reordered list of zones after dragging and dropping them. The reordering is done by a field on a zone object called 'DisplayOrder' The logic is setting the numbers correct. The problem is that when I try to mimic it with optimistic ui and update, there is a slight lag like its still waiting for the network.
Most of the meat of what i'm trying to achieve is happening at the onDragEnd = () => { ... } function.
import React, { Component } from "react";
import { graphql, compose, withApollo } from "react-apollo";
import gql from "graphql-tag";
import { withState } from "recompose";
import { withStyles } from "#material-ui/core/styles";
import Select from "#material-ui/core/Select";
import MenuItem from "#material-ui/core/MenuItem";
import Input from "#material-ui/core/Input";
import Grid from "#material-ui/core/Grid";
import InputLabel from "#material-ui/core/InputLabel";
import Tabs from "#material-ui/core/Tabs";
import Tab from "#material-ui/core/Tab";
import AppBar from "#material-ui/core/AppBar";
import _ from "lodash";
import FormControl from "#material-ui/core/FormControl";
import move from "lodash-move";
import { Zone } from "../../Components/Zone";
const style = {
ddlRight: {
left: "3px",
position: "relative",
paddingRight: "10px"
},
ddlDrop: {
marginBottom: "20px"
},
dropdownInput: {
minWidth: "190px"
}
};
class Zones extends Component {
constructor(props) {
super(props);
this.state = {
companyId: "",
districtId: "",
selectedTab: "Zones",
autoFocusDataId: null,
zones: [],
unassignedZone: null
};
}
handleChange = event => {
const { client } = this.props;
this.setState({ [event.target.name]: event.target.value });
};
handleTabChange = (event, selectedTab) => {
this.setState({ selectedTab });
};
onDragStart = () => {
this.setState({
autoFocusDataId: null
});
};
calculateLatestDisplayOrder = () => {
const { allZones } = this.state;
if (allZones.length === 0) {
return 10;
}
return allZones[allZones.length - 1].displayOrder + 10;
};
updateCitiesDisplayOrder = cities => {
let displayOrder = 0;
const reorderedCities = _.map(cities, aCity => {
displayOrder += 10;
const city = { ...aCity, ...{ displayOrder } };
if (city.ZonesCities) {
city.ZonesCities.displayOrder = displayOrder;
}
return city;
});
return reorderedCities;
};
moveAndUpdateDisplayOrder = (allZones, result) => {
const reorderedZones = _.cloneDeep(
move(allZones, result.source.index, result.destination.index)
);
let displayOrder = 0;
_.each(reorderedZones, (aZone, index) => {
displayOrder += 10;
aZone.displayOrder = displayOrder;
});
return reorderedZones;
};
/**
* droppable id board represents zones
* #param result [holds our source and destination draggable content]
* #return
*/
onDragEnd = result => {
console.log("Dragging");
if (!result.destination) {
return;
}
const source = result.source;
const destination = result.destination;
if (
source.droppableId === destination.droppableId &&
source.index === destination.index
) {
return;
}
const {
zonesByCompanyAndDistrict,
unassignedZoneByCompanyAndDistrict
} = this.props.zones;
// reordering column
if (result.type === "COLUMN") {
if (result.source.index < 0 || result.destination.index < 0) {
return;
}
const { reorderZones, companyId, districtId } = this.props;
const sourceData = zonesByCompanyAndDistrict[result.source.index];
const destinationData =
zonesByCompanyAndDistrict[result.destination.index];
const reorderedZones = this.moveAndUpdateDisplayOrder(
zonesByCompanyAndDistrict,
result
);
console.log(reorderedZones);
console.log(unassignedZoneByCompanyAndDistrict);
reorderZones({
variables: {
companyId,
districtId,
sourceDisplayOrder: sourceData.displayOrder,
destinationDisplayOrder: destinationData.displayOrder,
zoneId: sourceData.id
},
optimisticResponse: {
__typename: "Mutation",
reorderZones: {
zonesByCompanyAndDistrict: reorderedZones
}
},
// refetchQueries: () => ["zones"],
update: (store, { data: { reorderZones } }) => {
const data = store.readQuery({
query: unassignedAndZonesQuery,
variables: {
companyId,
districtId
}
});
store.writeQuery({
query: unassignedAndZonesQuery,
data: data
});
}
});
// this.setState({ zones: reorderedZones });
// Need to reorder zones api call here
// TODO: Elixir endpoint to reorder zones
}
return;
};
render() {
const { selectedTab } = this.state;
const {
classes,
companies,
districts,
companyId,
districtId,
setCompanyId,
setDistrictId,
zones
} = this.props;
const isDisabled = !companyId || !districtId;
return (
<Grid container spacing={16}>
<Grid container spacing={16} className={classes.ddlDrop}>
<Grid item xs={12} className={classes.ddlRight}>
<h2>Company Zones</h2>
</Grid>
<Grid item xs={2} className={classes.ddlRight}>
<FormControl>
<InputLabel htmlFor="company-helper">Company</InputLabel>
<Select
value={companyId}
onChange={event => {
setCompanyId(event.target.value);
}}
input={
<Input
name="companyId"
id="company-helper"
className={classes.dropdownInput}
/>
}
>
{_.map(companies.companies, aCompany => {
return (
<MenuItem
value={aCompany.id}
key={`companyItem-${aCompany.id}`}
>
{aCompany.name}
</MenuItem>
);
})}
</Select>
</FormControl>
</Grid>
<Grid item xs={2} className={classes.ddlRight}>
<FormControl>
<InputLabel htmlFor="district-helper">District</InputLabel>
<Select
value={districtId}
onChange={event => {
setDistrictId(event.target.value);
}}
input={
<Input
name="districtId"
id="district-helper"
className={classes.dropdownInput}
/>
}
>
{_.map(districts.districts, aDistrict => {
return (
<MenuItem
value={aDistrict.id}
key={`districtItem-${aDistrict.id}`}
>
{aDistrict.name}
</MenuItem>
);
})}
</Select>
</FormControl>
</Grid>
</Grid>
<Grid container>
<AppBar position="static" color="primary">
<Tabs value={selectedTab} onChange={this.handleTabChange}>
<Tab value="Zones" label="Zone" disabled={isDisabled} />
<Tab
value="Pricing Structure"
label="Pricing Structure"
disabled={isDisabled}
/>
<Tab value="Pricing" label="Pricing" disabled={isDisabled} />
<Tab
value="Student Pricing"
label="Student Pricing"
disabled={isDisabled}
/>
</Tabs>
</AppBar>
{selectedTab === "Zones" &&
zones &&
zones.zonesByCompanyAndDistrict && (
<Zone
onDragStart={this.onDragStart}
onDragEnd={this.onDragEnd}
zones={_.sortBy(zones.zonesByCompanyAndDistrict, [
"displayOrder"
])}
unassignedZone={zones.unassignedZoneByCompanyAndDistrict}
/>
)}
{selectedTab === "Pricing Structure" && <div>Pricing Structure</div>}
{selectedTab === "Pricing" && <div>Pricing</div>}
{selectedTab === "Student Pricing" && <div>Student Pricing</div>}
</Grid>
</Grid>
);
}
}
const companiesQuery = gql`
query allCompanies {
companies {
id
name
}
}
`;
const districtsQuery = gql`
query allDistricts {
districts {
id
name
}
}
`;
const unassignedAndZonesQuery = gql`
query zones($companyId: String!, $districtId: String!) {
unassignedZoneByCompanyAndDistrict(
companyId: $companyId
districtId: $districtId
) {
name
description
displayOrder
cities {
id
name
}
}
zonesByCompanyAndDistrict(companyId: $companyId, districtId: $districtId) {
id
name
description
displayOrder
basePrice
zoneCities {
displayOrder
city {
id
name
}
}
}
}
`;
const reorderZones = gql`
mutation(
$companyId: String!
$districtId: String!
$sourceDisplayOrder: Int!
$destinationDisplayOrder: Int!
$zoneId: String!
) {
reorderZones(
companyId: $companyId
districtId: $districtId
sourceDisplayOrder: $sourceDisplayOrder
destinationDisplayOrder: $destinationDisplayOrder
zoneId: $zoneId
) {
id
__typename
name
description
displayOrder
basePrice
zoneCities {
displayOrder
city {
id
name
}
}
}
}
`;
export default compose(
withState("companyId", "setCompanyId", ""),
withState("districtId", "setDistrictId", ""),
graphql(unassignedAndZonesQuery, {
name: "zones",
skip: ({ companyId, districtId }) => !(companyId && districtId),
options: ({ companyId, districtId }) => ({
variables: { companyId, districtId },
fetchPolicy: "cache-and-network"
})
}),
graphql(companiesQuery, {
name: "companies",
options: { fetchPolicy: "cache-and-network" }
}),
graphql(districtsQuery, {
name: "districts",
options: { fetchPolicy: "cache-and-network" }
}),
graphql(reorderZones, {
name: "reorderZones"
}),
withApollo,
withStyles(style)
)(Zones);
https://drive.google.com/file/d/1ujxTOGr0YopeBxrGfKDGfd1Cl0HiMaK0/view?usp=sharing <- this is a video demonstrating it happening.
For anyone who comes across this same issue, the main problem was that my update / optimisticResponse were both not correct. Something to mention here is this block:
update: (store, { data: { reorderZones } }) => {
const {
zonesByCompanyAndDistrict,
unassignedZoneByCompanyAndDistrict
} = store.readQuery({
query: unassignedAndZonesQuery,
variables: {
companyId,
districtId
}
});
const reorderedZones = this.moveAndUpdateDisplayOrder(
zonesByCompanyAndDistrict,
result
);
store.writeQuery({
query: unassignedAndZonesQuery,
variables: { companyId, districtId },
data: {
unassignedZoneByCompanyAndDistrict,
zonesByCompanyAndDistrict: reorderedZones
}
});
}
If you compare it to my original code up top, you see that when I writeQuery I have variables this time. Looking with the apollo devtools, I saw that there was a entry added, just one with the wrong variables. So that was a easy fix. The optimistic response was correct (mimics the payload returned from our mutation). The other aspect that was wrong, was that my query for fetching all this data initially had a fetch policy of cache and network, what this means is that when we receive data we cache it, and we ask for the latest data. So this will always fetch the latest data. I didn't need that, hence the little lag coming, I just needed optimisticResponse. By default apollo does cache-first, where it looks in the cache for data, if its not there we grab it via the network. Pairs well with cache updates and slow nets.

Resources