I have a child component like so:
import { skipToken } from "#reduxjs/toolkit/dist/query";
import moment from "moment";
import {
useFetchAccountQuery,
useFetchTransactionsQuery,
} from "./accountApiSlice";
const TransactionsTable = () => {
const { data: accountData } = useFetchAccountQuery();
const changesSince: string = moment().subtract(7, "d").toISOString();
let params = {
accountId: accountData?.accountId,
categoryId: accountData?.defaultCategoryId,
changesSince,
};
const paramsAreGood: boolean = params.accountId && params.categoryId ? true : false;
const { data: transactions, isSuccess: transactionsSuccess } = useFetchTransactionsQuery(paramsAreGood ? params : skipToken);
return (
<>
{transactionsSuccess && (
<div>
<h1>Number of transactions: {transactions?.length}</h1>
</div>
)}
</>
);
};
export default TransactionsTable;
I am making two queries, then using the accountId and categoryId from the 1st query to pass to the 2nd query so that I can make the transactions query and get the data.
In Chrome's Network tab, I can see that my useFetchTransactionsQuery() fires and I get data back.
However, my component remains blank and I do not get:
<h1>Number of transactions: <some number></h1>
Here is a screenshot from Redux Dev Tools too:
What am I missing?
I think before you set values to param you should check if useFetchAccountQuery has returned any data yet, as useFetchAccountQuery is an async function and i think the param value is set before account data is fatched
try it like this
import { skipToken } from "#reduxjs/toolkit/dist/query";
import moment from "moment";
import {
useFetchAccountQuery,
useFetchTransactionsQuery,
} from "./accountApiSlice";
const TransactionsTable = () => {
const { data: accountData, isSucceess: AccountDataSuccess } = useFetchAccountQuery();
const changesSince: string = moment().subtract(7, "d").toISOString();
let params ;
if(AccountDataSuccess && accountData ){
params = {
accountId: accountData?.accountId,
categoryId: accountData?.defaultCategoryId,
changesSince,
}
};
const paramsAreGood: boolean = params.accountId && params.categoryId ? true : false;
const { data: transactions, isSuccess: transactionsSuccess } = useFetchTransactionsQuery(paramsAreGood ? params : skipToken);
return (
<>
{transactionsSuccess && (
<div>
<h1>Number of transactions: {transactions?.length}</h1>
</div>
)}
</>
);
};
export default TransactionsTable;
Please annotate the param accordingly if your are using typescript.
I hope this works. I use this type of pattern in my work and it works.
The culprit was this line:
const changesSince: string = moment().subtract(7, "d").toISOString();
I've no idea why, but I ditched moment for dayjs and it works now.
Related
I need to be able to set and access cookies in my Gatsby project, and I was able to get something solid setup using this tutorial. I'm building a hook that sets a cookie, and utilizing it throughout the site. This is what the helper looks like when it's all said and done.
use-cookie.ts
import { useState, useEffect } from 'react';
const getItem = (key) =>
document.cookie.split('; ').reduce((total, currentCookie) => {
const item = currentCookie.split('=');
const storedKey = item[0];
const storedValue = item[1];
return key === storedKey ? decodeURIComponent(storedValue) : total;
}, '');
const setItem = (key, value, numberOfDays) => {
const now = new Date();
// set the time to be now + numberOfDays
now.setTime(now.getTime() + numberOfDays * 60 * 60 * 24 * 1000);
document.cookie = `${key}=${value}; expires=${now.toUTCString()}; path=/`;
};
/**
*
* #param {String} key The key to store our data to
* #param {String} defaultValue The default value to return in case the cookie doesn't exist
*/
export const useCookie = (key, defaultValue) => {
const getCookie = () => getItem(key) || defaultValue;
const [cookie, setCookie] = useState(getCookie());
const updateCookie = (value, numberOfDays) => {
setCookie(value);
setItem(key, value, numberOfDays);
};
return [cookie, updateCookie];
};
I'm calling the hook into a component like so:
DealerList.tsx
import React, { ReactNode, useEffect } from 'react';
import { Container } from 'containers/container/Container';
import { Section } from 'containers/section/Section';
import { Link } from 'components/link/Link';
import s from './DealerList.scss';
import { useCookie } from 'hooks/use-cookie';
interface DealerListProps {
fetchedData: ReactNode;
}
let cookie;
useEffect(() => {
cookie = useCookie();
}, []);
export const DealerList = ({ fetchedData }: DealerListProps) => {
const dealerInfo = fetchedData;
if (!dealerInfo) return null;
const [cookie, updateCookie] = useCookie('one-day-location', 'sacramento-ca');
return (
<>
<Section>
<Container>
<div className={s.list}>
{dealerInfo.map((dealer: any) => (
<div className={s.dealer} key={dealer.id}>
<div className={s.dealer__info}>
<h3 className={s.name}>
{dealer.company.name}
</h3>
<span className={s.address}>{dealer.address.street}</span>
<span className={s.city}>{dealer.address.city} {dealer.address.zip}</span>
</div>
<div className={s.dealer__contact}>
<span className={s.email}>{dealer.email}</span>
<span className={s.phone}>{dealer.phone}</span>
</div>
<div className={s.dealer__select}>
<Link
to="/"
className={s.button}
onClick={() => {
updateCookie(dealer.phone, 10);
}}
>
Select Location
</Link>
</div>
</div>
))}
</div>
</Container>
</Section>
</>
);
};
It works well on gatsby develop and I'm able to access the value of the cookie and change the contact information that's displayed accordingly. However, when I try and build, or push to Netlify, I'm getting this error.
WebpackError: ReferenceError: document is not defined
I know this has something to do with document.cookie on lines 4 and 17, but I'm struggling trying to figure out how to fix it. Any suggestions? I'm imported useEffect, and from my research that has something to do with it, but what can I do to get it working properly?
Thanks in advance.
I did a bit more research, and I found this simple hook, replaced the code in use-cookie.ts with this, made a few modifications to it (included below), installed universal-cookie and it seems to work perfectly. Here's the new code:
use-cookie.ts
import { useState } from 'react';
import Cookies from 'universal-cookie';
export const useCookie = (key: string, value: string, options: any) => {
const cookies = new Cookies();
const [cookie, setCookie] = useState(() => {
if (cookies.get(key)) {
return cookies.get(key);
}
cookies.set(key, value, options);
});
const updateCookie = (value: string, options: any) => {
setCookie(value);
removeItem(value);
cookies.set(key, value, options);
};
const removeItem = (key: any) => {
cookies.remove(key);
};
return [cookie, updateCookie, removeItem];
};
If anyone has a better way to do this though, please let me know!
According to Gatsby's Debugging HTML Builds documentation:
Some of your code references “browser globals” like window or
document. If this is your problem you should see an error above like
“window is not defined”. To fix this, find the offending code and
either a) check before calling the code if window is defined so the
code doesn’t run while Gatsby is building (see code sample below) or
b) if the code is in the render function of a React.js component, move
that code into a componentDidMount lifecycle or into a useEffect hook,
which ensures the code doesn’t run unless it’s in the browser.
So, without breaking the rule of hooks, calling a hook inside another hook, causing a nested infinite loop. You need to ensure the document creation before calling it. Simply by adding a checking condition:
import { useState } from 'react';
const getItem = (key) => {
if (typeof document !== undefined) {
document.cookie.split(`; `).reduce((total, currentCookie) => {
const item = currentCookie.split(`=`);
const storedKey = item[0];
const storedValue = item[1];
return key === storedKey ? decodeURIComponent(storedValue) : total;
}, ``);
}
};
const setItem = (key, value, numberOfDays) => {
const now = new Date();
// set the time to be now + numberOfDays
now.setTime(now.getTime() + numberOfDays * 60 * 60 * 24 * 1000);
if (typeof document !== undefined) {
document.cookie = `${key}=${value}; expires=${now.toUTCString()}; path=/`;
}
};
/**
*
* #param {String} key The key to store our data to
* #param {String} defaultValue The default value to return in case the cookie doesn't exist
*/
export const useCookie = (key, defaultValue) => {
const getCookie = () => getItem(key) || defaultValue;
const [cookie, setCookie] = useState(getCookie());
const updateCookie = (value, numberOfDays) => {
setCookie(value);
setItem(key, value, numberOfDays);
};
return [cookie, updateCookie];
};
Since you might be calling useCookie custom hook in a component or page where the document doesn't exist yet, I would double-check by using the same condition or using a useEffect with empty dependencies ([], now it won't break the rule of hooks):
const Index = props => {
let cookie;
// both works
if (typeof document !== undefined) {
cookie = useCookie();
}
useEffect(() => {
cookie = useCookie();
}, []);
return <Layout>
<Seo title="Home" />
<h1>Hi people</h1>
</Layout>;
};
I have a problem in the following component, it seems that the component doesn't render and I get the following error in console: "Cannot read property 'operationalHours' of null". I don't get why operationalHours it's null.. maybe someone can help me with a posible solution for this issue.
Here is the component:
import React, { useState, useEffect } from 'react';
import Search from 'client/components/ui/Search';
import { performSearchById } from 'client/actions/api/search';
import { get } from 'lodash';
import {
SEARCH_STORE_NOT_CLOSED,
SEARCH_STORE_OPEN_TEXT,
SEARCH_STORE_CLOSED_TEXT
} from 'app/client/constants/values';
import DownArrow from 'components/UI/icons/DownArrow';
import styles from './styles.module.scss';
const StoreDetails = ({ storeInfo }) => {
const [expanded, setIsExpanded] = useState(false);
const [storeData, setStoreData] = useState(null);
useEffect(() => {
async function fetchData() {
const storeId = storeInfo.store_id;
const {
data: {
Location: {
contactDetails: { phone },
operationalHours
}
}
} = await performSearchById(storeId);
setStoreData({ phone, operationalHours });
}
fetchData();
}, [storeInfo.store_id]);
const infoText = expanded ? 'Hide details' : 'View details';
function parseHours(hours) {
const formattedHours = {};
hours.forEach(dayObj => {
const closed = get(dayObj, 'closed', '');
const day = get(dayObj, 'day', '');
if (closed === SEARCH_STORE_NOT_CLOSED) {
const openTime = get(dayObj, 'openTime', '');
const closeTime = get(dayObj, 'closeTime', '');
if (openTime === null || closeTime === null) {
formattedHours[day] = SEARCH_STORE_OPEN_TEXT;
} else {
formattedHours[day] = `${openTime}-${closeTime}`;
}
} else {
formattedHours[day] = SEARCH_STORE_CLOSED_TEXT;
}
});
return formattedHours;
}
const storeHours = storeData.operationalHours
? parseStoreHours(storeData.operationalHours)
: '';
return (
<div className={styles.viewStoreDetails}>
<span
className={expanded ? styles.expanded : undefined}
onClick={() => setIsExpanded(!expanded)}
>
<DownArrow />
</span>
<div>
<span className={styles.viewStoreDetailsLabel}>{infoText}</span>
<div>
{expanded && (
<Search
phoneNumber={storeData.phone}
storeHours={storeHours}
/>
)}
</div>
</div>
</div>
);
};
export default StoreDetails;
Its because you're setting the values of storeData after the component has already rendered the first time. Your default value for storeData is null.
It breaks here: storeData.operationalHours because null isn't an object and therefore cannot have properties to access on it.
You should probably just set your initial state to something more representative of your actual state:
const [storeData, setStoreData] = useState({}); // Or even add keys to the object.
Also read here about the useEffect hook and when it runs. It seems that the underlying issue is misunderstanding when your data will be populated.
You are getting error at this line :
const storeHours = storeData.operationalHours ?
parseStoreHours(storeData.operationalHours): '';
Reason : You initialised storeData as Null and you are trying to access operationalHours key from Null value.
Correct Way is :
Option 1: Initialise storeData as blank object
const [storeData, setStoreData] = useState({});
Option 2:
const storeHours =storeData && storeData.operationalHours ?
parseStoreHours(storeData.operationalHours): '';
It's happen because in 1st moment of your application, storeData is null, and null don't have properties, try add a empty object as first value ({}) or access a value like that:
Correct method:
const object = null;
console.log(object?.myProperty);
// output: undefined
Wrong method:
const object = null;
console.log(object.myProperty);
// Generate a error
The Question Mark(?) is a method to hidden or ignore if the variable are a non-object, to decrease verbosity in the code with logic blocks try and catch, in Correct method code there will be no mistake, but in Wrong method code, there will have a mistake.
Edit 1:
See more here
I'm working with two API endpoints. The first one returns a list of dates in a string format for which data is available. The date can then be added to the second endpoint and renders additional data. On the Graphql Playground I have been able to make it all work. On the front-end I have a select option drop down for the dates, but I have not been able to fire off the second API call when I click on any given date. It's the first time I'm using graphql mutation and I haven't been able to get the second API request to return any data when I select a date. Thank you.
Front-end code:
app.tsx
import * as React from 'react'
import { useState } from 'react'
import { useMutation } from '#apollo/react-hooks'
import { IrriSatQuery } from '../../generated/graphql'
import { MAP_LAYER } from './query'
interface Props {
data: IrriSatQuery;
}
const IrriSat: React.FC<Props> = ({ data }) => {
const [option, setOption] = useState((data?.mapDates as any)[0].date!)
const [getLayer] = useMutation(MAP_LAYER)
return (
<>
<ContentWrapper>
<select value={option} onChange={( e: React.ChangeEvent<HTMLSelectElement>, ): void => {setOption(e.target.value, getLayer(e.target.value)}} onSelect={() => getLayer({variables: {type: option}})}>
{data?.mapDates?.slice(0,52).map(res =>
<option key={res?.date!} value={res?.date!}>{res?.date}</option>
)
}
</select>
</ContentWrapper>
</>
)
}
export default IrriSat
query.ts
export const QUERY_IRR_SAT = gql`
query IrriSat {
mapDates {
date
dateurl
}
}
`
export const MAP_LAYER = gql`
mutation MapLayer($date: String!) {
mapDate(date: $date) {
token
mapid
name
}
}
`
Back-end code:
server.js
class IrriSatAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://irrisat-cloud.appspot.com/_ah/api/irrisat/v1/services/'
}
async getMapsDates() {
const response = await this.get('maps/dates')
return Array.isArray(response.items) ? response.items.map(response => this.mapsDatesReducer(response)) : []
}
mapsDatesReducer(response) {
return {
date: response.date,
dateurl: response.dateurl,
}
}
async getMapsLayer(date) {
const response = await this.get(`maps/layers/${date}`)
return Array.isArray(response.items) ? response.items.map(response => this.mapsLayerReducer(response)) : []
}
mapsLayerReducer(response) {
return {
token: response.token,
mapid: response.mapid,
name: response.name
}
}
}
}
schema.js
type MapDates {
date: String
dateurl: String
}
type Mutation {
mapDate(date: String): [MapsLayers]
}
type Query {
mapDates: [MapDates]
resolver.js
module.exports = {
Query: {
mapDates: (_, __, { dataSources }) => dataSources.irriSatAPI.getMapsDates(),
},
Mutation: {
mapDate: (_, { date }, { dataSources }) => dataSources.irriSatAPI.getMapsLayer(date)
}
}
There are a few issues with your onChange function.
You are calling getLayer twice? You should only need to call it once, at the same time as you set the value of the dropdown. Also, as far as I know, you don't really need the onSelect.
import * as React from 'react';
import { useState } from 'react';
import { useMutation } from '#apollo/react-hooks';
import gql from 'graphql-tag';
const MAP_LAYER = gql`
mutation MapLayer($date: String!) {
mapDate(date: $date) {
token
mapid
name
}
}
`;
const ContentWrapper = ({ children }) => <div>{...children}</div>;
const IrriSat: React.FC<any> = ({ data }) => {
const [option, setOption] = useState((data?.mapDates as any)[0].date!);
const [getLayer]: any = useMutation(MAP_LAYER);
return (
<ContentWrapper>
<select
value={option}
onChange={(e: React.ChangeEvent<HTMLSelectElement>): void => {
setOption(e.target.value);
getLayer({ variables: { date: e.target.value } });
}}
>
{data?.mapDates?.slice(0, 52).map(res => (
<option key={res?.date!} value={res?.date!}>
{res?.date}
</option>
))}
</select>
</ContentWrapper>
);
};
export default IrriSat;
Obviously, I changed a few things to get rid of some of the editor warnings, but pay particular attention to the onChange property.
A tip: you are probably experiencing these issues due to the extreme length of the line that you add all of this logic condensed into. Install the "Prettier - Code formatter" VS Code extension. Enable VS Code's format on save option. Profit.
I am trying to fetch images by their ids. The architecture of backend is as follows: DB stores images in binary and there is another table that stores images ids.
I am using apollo client on front end to prefetch images ids and then send another set of fetch requests.
Unfortunately I get Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. Could anyone help me to
1) figure out why it happens. I see that there is bunch of pending promises in the stack.
and 2) how it can be refactored to better architecture.
import React, {useState} from 'react'
import {useQuery} from "#apollo/react-hooks";
import {gql} from 'apollo-boost';
const apiEndpoint = 'http://localhost:9211';
const getProductImage = function (id) {
return gql`
{
productById(id: "${id}") {
images {
imageId
}
}
}`
};
const fetchImage = (imageUrl, allImgsArr) => {
return fetch(imageUrl)
.then(res => res.blob())
.then(img => allImgsArr.push(URL.createObjectURL(img)))
};
const ItemPage = (props) => {
const [id] = useState(props.match.params.id);
const {data} = useQuery(getProductImage(id));
let imagesIds = [];
if (data) {
data.productById.images.forEach(image => {
imagesIds.push(image.imageId)
});
}
const [imagesUrls, setImagesUrl] = useState([]);
// MULTIPE FETCH RETRIEVALS START
for (let imId of imagesIds) {
setImagesUrl(imagesUrls => [...imagesUrls, fetchImage(`${apiEndpoint}/image/${imId}`, imagesUrls)]);
}
// MULTIPE FETCH RETRIEVALS END
return (
<>
<div>
<div>
<img src={imagesUrls[0] ? imagesUrls[0] : ''} alt="main item 1 photo"/>
</div>
<div>
<div>
<img src={imagesUrls[1] ? imagesUrls[1] : ''} alt="Additional item 1 photo"/>
</div>
</div>
</div>
</>
)
};
export default ItemPage;
your query should be a constant , not function.
const GET_PRODUCT_IMAGE = gql`
query getProduct($id:String!) {
productById(id: $id) {
images {
imageId
}
}
}
}`
// pass variables like this
const {data} = useQuery(GET_PRODUCT_IMAGE, { variables: { id },
});
More Info : https://www.apollographql.com/docs/react/data/queries/
I have a component that uses axios to access the PubMed api (in componentDidMount), retrieves some publication ids then stores them in state as "idlist". A second function is then called (addPapers) which passes in this id list and makes a second api call to retrieve further details (title, journal, authors) for each id. All this seems to work fine and when I use react tools to check state there is an array ("paperList") full of objects that have the expected key:value pairs. However, when I try to map over this array and access the values within the objects in the render function (ie paper.title, paper.author, paper.journal) they are returning as undefined. I haven't been using react for long and suspect I am making a basic mistake but cant figure it out.
I have tried console.logging each step and the expected data is in state and correct in react tools
import axios from 'axios'
import './App.css';
import rateLimit from 'axios-rate-limit';
class App extends Component {
state= {
idlist: [],
papersList : ""
}
componentDidMount () {
console.log("incomponent")
axios.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&retmode=json&retmax=1000&term=((Australia%5Bad%5D)%20AND%20(%222019%2F07%2F01%22%5BDate%20-%20Publication%5D%20%3A%20%223000%22%5BDate%20-%20Publication%5D))%20AND%20(%22nature%22%5BJournal%5D%20OR%20%22Nature%20cell%20biology%22%5BJournal%5D%20OR%20%22Nature%20structural%20%26%20molecular%20biology%22%5BJournal%5D)")
.then (response =>
this.setState({idlist: response.data.esearchresult.idlist}, () => {
this.addPapers(this.state.idlist)
}
)
)}
addPapers = (idlist) => {
if (idlist) {
const http = rateLimit(axios.create(), { maxRequests: 6, perMilliseconds: 1000 })
const list = this.state.idlist.map(id => {
let paperObj ={};
let paperList =[]
http.get(`https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&rettype=abstract&id=${id}&api_key=9476810b14695bd14f228e63433facbf9c08`)
.then (response2 => {
const title = response2.data.result[id].title
const journal = response2.data.result[id].fulljournalname
const authorList = []
const authors = response2.data.result[id].authors
authors.map((author, idx) =>
idx > 0 ? authorList.push(" " + author.name) : authorList.push(author.name))
paperObj.title = title
paperObj.journal = journal
paperObj.authors = authorList.toString()
paperList.push(paperObj)
})
return paperObj
})
this.setState({papersList: list})
}
}
render () {
let article = ""
if (this.state.papersList.length){
article = this.state.papersList.map(paper =>
console.log (paper.title)
console.log (paper.authors)
console.log (paper.journal)
)
}
return (
<div className="App">
<h1>Publications</h1>
{article}
</div>
);
}
}
export default App;
I expect that when I map over paperList and extract each paper I should be able to return the title, journal or authors using console.log(paper.title), console.log(paper.title), console.log(paper.title). These are all returning undefined.
You have two issues in code
1) paperList array declaration should be out of map loop.
2) paperList should be returned instead of paperObj
Working code below make some enhancements in render function
Also codesandbox link
import React from "react";
import ReactDOM from "react-dom";
import rateLimit from "axios-rate-limit";
import axios from "axios";
import "./styles.css";
class App extends React.Component {
state = {
idlist: [],
papersList: ""
};
componentDidMount() {
console.log("incomponent");
axios
.get(
"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&retmode=json&retmax=1000&term=((Australia%5Bad%5D)%20AND%20(%222019%2F07%2F01%22%5BDate%20-%20Publication%5D%20%3A%20%223000%22%5BDate%20-%20Publication%5D))%20AND%20(%22nature%22%5BJournal%5D%20OR%20%22Nature%20cell%20biology%22%5BJournal%5D%20OR%20%22Nature%20structural%20%26%20molecular%20biology%22%5BJournal%5D)"
)
.then(response =>
this.setState({ idlist: response.data.esearchresult.idlist }, () => {
this.addPapers(this.state.idlist);
})
);
}
addPapers = idlist => {
if (idlist) {
const http = rateLimit(axios.create(), {
maxRequests: 6,
perMilliseconds: 1000
});
let paperList = [];
this.state.idlist.forEach(id => {
let paperObj = {};
http
.get(
`https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&rettype=abstract&id=${id}&api_key=9476810b14695bd14f228e63433facbf9c08`
)
.then(response2 => {
const title = response2.data.result[id].title;
const journal = response2.data.result[id].fulljournalname;
const authorList = [];
const authors = response2.data.result[id].authors;
authors.map((author, idx) =>
idx > 0
? authorList.push(" " + author.name)
: authorList.push(author.name)
);
paperObj.title = title;
paperObj.journal = journal;
paperObj.authors = authorList.toString();
paperList.push(paperObj);
})
.then(result => {
this.setState({ papersList: paperList });
});
});
}
};
render() {
return (
<div className="App">
<h1>Publications</h1>
{this.state.papersList.length &&
this.state.papersList.map(data => {
return <div>{data.title}</div>;
})}
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Hope it helps!!!
Do it like this:
render () {
let article;
if (this.state.papersList.length){
article = this.state.papersList.map(paper => <p>span>Title is {paper.title}</span></p> )
}
return (
<div className="App">
<h1>Publications</h1>
{article}
</div>
);
}