GraphQL Query Crashing On "If" Working on "Else If" - javascript

I am trying to access a query from my frontend react page. When I call the query this
import React from "react";
import { useQuery } from "#apollo/react-hooks";
import { getBookQuery } from "../queries/queries";
export const BookDetails = (props) => {
const { loading, error, data } = useQuery(getBookQuery, {
skip: !props.bookId,
variables: { id: props.bookId },
});
let content;
if (loading) content = <p>Loading Detail...</p>;
if (error) {
content = <p>Error...</p>;
}
if (!props.bookId) {
content = <p>No book selected yet</p>;
} else {
console.log(data.book);
const book = data;
}
return (
<div id="hook-details">
</div>
);
When I run this code I see the error TypeError: Cannot read property 'book' of undefined
To fix this error I changed the above code to use else if statements instead of just if statements, but I am failing to understand why this fixes the bug. Even when I throw log statements in each if block the same code seems to be called at the same time.
Working Code:
import React from "react";
import { useQuery } from "#apollo/react-hooks";
import { getBookQuery } from "../queries/queries";
export const BookDetails = (props) => {
const { loading, error, data } = useQuery(getBookQuery, {
skip: !props.bookId,
variables: { id: props.bookId },
});
let content;
if (loading) content = <p>Loading Detail...</p>;
else if (error) {
content = <p>Error...</p>;
}
else if (!props.bookId) {
content = <p>No book selected yet</p>;
} else {
console.log(data.book);
const book = data;
}
return (
<div id="hook-details">
</div>
);
};
Thank you.

1st case
... when your component gets bookId prop following code works:
if (loading) content = <p>Loading Detail...</p>;
and
} else {
console.log(data.book);
const book = data;
}
data is undefined at start then error appears.
2nd case
... chained if else excludes the last fragment (console.log(data.book)) until data is loaded - loading OR data access, no both (AND)
When loading gets false then data contains book, it can be safely logged out.
PS. It's safer to use 'early return':
if( !props.bookId ) return <NoBookSelectedMessage /> // or null to show nothing
if( loading ) return <Loading/>
if( error ) return <Error />
// 'data' must be defined here

The problem is data value arrives asynchronously meanwhile you are expecting synchronously. Try using useEffect hook to catch changes on data after useQuery returned the value.
Like the following - by passing data into the dependency array:
useEffect(() => {
if (data) {
// your actions here
console.log(data.book);
}
}, [data]);
Suggested read: Using the Effect Hook
I hope this helps!

Related

React hook returns before it should

I’ve encountered a problem with a React hook from the "react-moralis" SDK and need some help.
I’ve implemented the useWeb3Contract hook from the package in an exported function, which should return the result (data) of the function call after fetching the data successfully. However, the function seems to be returning before the data gets fetched successfully.
Here is the code of the function:
import { useEffect } from "react"
import { useWeb3Contract } from "react-moralis"
import { abi as tokenAbi } from "../../constants/ERC20"
export default function useRetrieveTokenSymbol(address) {
const {
runContractFunction: fetch,
data,
isFetching,
isLoading,
} = useWeb3Contract({
abi: tokenAbi,
contractAddress: address,
functionName: "symbol",
params: {},
})
const retrieve = async () => {
await fetch()
}
useEffect(() => {
retrieve()
}, [])
if (!isFetching && !isLoading && data) return data
}
This function then gets called in a separate react component, where it should only return the data from the useWeb3Contract hook, which then can be stored in a variable.
That looks like that:
export default function TokenSymbol({tokenAddress}) {
const tokenSymbol = useRetrieveTokenSymbol(tokenAddress)
return (
<div>{tokenSymbol}</div>
)
}
tokenSymbol however always is undefined, because the useRetrieveTokenSymbol function returns before finishing fetching the data and then returns an undefined data constant. The conditional if statement gets completely ignored and I simply cannot figure out why.
Any of you guys know a fix to this? Would really appreciate it :)
You should wrap the values you are tracking in state. That way, when they update, the hook will cause a re-render:
const [myData, setMyData] = useState(data);
useEffect(() => {
setMyData(data);
}, [data]);
return myData;

React: following DRY in React when fetching data from API

When I am fetching the data from API, If I get Ok I will render the component to display the data,
If an error has occured, I want to display a custom message for different components, and during fetching the data I want to show a loading message.
export default function App() {
const { data: products, loading, error } = useFetch(
"products?category=shoes"
);
if (error) return <div>Failed to load</div>;
if (loading) return <div>wait is Loading</div>;
return (
<>
<div className="content">
<section id="products">{products.map((product) =>{return <div>product.name</div>})}</section>
</div>
</>
);
}
I am planning to have a fetch call in multiple components. So, I was thinking If I could write the above logic in one place, like an HOC or Rendered Props and reuse it.
What I tried: but failed
HOC wont work because you cant call hooks in normal functions. It has to be a Component.
Rendered Props wont work as it gives this error:
Error: Rendered more hooks than during the previous render.
Below is my failed rendered props code
const ShowData= function ({ data: products }) {
return (
<div>
{products.map(product => {return <div>product.name</div>})}
</div>
);
};
function SearchCount({ count }) {
return <h3>Search Results: {count} items found</h3>;
}
const Wrapper = function () {
return (
<LoadingAndError
render={ShowData}
params={{ url: "products?category=shoes" }}
></LoadingAndError>
);
};
export default Wrapper;
Loading and error logic is moved to the LoadingAndError component
const LoadingAndError = function (props) {
const { url } = props.params;
const { data: products, loading,error} = useFetch(url);
if (loading)
return <h1>Loading</h1>;
if (error) return <h1>Error</h1>;
return props.render({ data: products });
};
return props.render({ data: products });
This line is calling props.render as a function, but you've passed in a component. Calling component directly can cause exactly the error you're seeing. If you want props.render to be used like this, then you should pass in a function which creates an element, like this:
<LoadingAndError
render={(props) => <ShowData {...props} />}
params={{ url: "products?category=shoes" }}
></LoadingAndError>
Alternatively, if you want to keep passing in render={ShowData}, then change loadingAndError to create an element:
const loadingAndError = function (props) {
const { url } = props.params;
const { data: products, loading,error} = useFetch(url);
if (loading)
return <h1>Loading</h1>;
if (error) return <h1>Error</h1>;
const Component = props.render
return <Component data={products}/>
}

React Store does not return getMethod's value first time

I am new to React, Mobx stack. I bumped into a problem where my Store does not reflect its value immediately after the first call. when I make the second call, it returns the first value. I am unable to understand why it happens since I have a long way to go in React, Mobx stack.
My Store Class
export default class ServiceTemplateStore {
loadingInitial = false;
serviceTemplateDetail: any | undefined;
constructor() {
makeAutoObservable(this)
}
setLoadingInitial = (state: boolean) => {
this.loadingInitial = state;
}
get getServiceTemplateDetail() {
return this.serviceTemplateDetail;
}
loadServiceTemplateDetail = async (id: string) => {
this.loadingInitial = true;
try {
const serviceTemplateDetail = await agent.serviceTemplateDetail.details(id);
runInAction(() => {
console.log("Service loaded");
this.serviceTemplateDetail = serviceTemplateDetail;
console.log(this.serviceTemplateDetail); //Here I can see it immediately outputs the value.
this.setLoadingInitial(false);
});
} catch (error) {
console.log(error);
this.setLoadingInitial(false);
}
}
}
Child component
Here is where I am using the Store, when I use the useEffect Hook it actually populates the data, but when I run in inside onClick function it does not populate the value.
What my understanding is that Axios GET call executes as expected, also it returns the value back and assigns it to serviceTemplateDetail variable. but when I am accessing the value via getServiceTemplateDetail method it returns null for the first time, and when I click the Import Button next time it returns the old value which was returned from GET Request
import DataGrid, { Selection } from "devextreme-react/data-grid";
import Popup, { Position, ToolbarItem } from "devextreme-react/popup";
import { Column } from "devextreme-react/tree-list";
import { observer } from "mobx-react-lite";
import React, { Component, useCallback, useEffect, useState } from 'react'
import { useStore } from "../store/Store";
export default observer(function ImportServiceTemplate(props: importServiceProp) {
const [ serviceTemplateIds, setServiceTemplateIds] = useState([]);
const { serviceTemplateStore } = useStore();
const { serviceTemplateList, loadServiceTemplateList, loadServiceTemplateDetail, getServiceTemplateDetail, serviceTemplateDetail } = serviceTemplateStore;
useEffect(() => {
if (serviceTemplateList?.length === 0 || serviceTemplateList === undefined) loadServiceTemplateList();
}, [loadServiceTemplateList])
const closeButtonOption = {
icon: 'close',
text: 'Close',
onClick: ()=>{
props.onCloseImportModel();
}
};
//Added a useEffect to test..
//UseEffect returns the value but not my Import function below
useEffect(() => {
console.log("use effect");
console.log(JSON.stringify(getServiceTemplateDetail));
}, [getServiceTemplateDetail])
//Actual function that needs to work.
const importButtonOption = {
icon: 'download',
text: 'Import',
onClick: () => {
if(serviceTemplateIds){
loadServiceTemplateDetail(String(serviceTemplateIds))
.then(()=>{
console.log("Callback");
console.log(JSON.stringify(serviceTemplateDetail)); // Getting undefined as output.
props.importingServiceTemplate(getServiceTemplateDetail); //Passing the imported value to parent component
});
}
}
};
const onSelectionChanged = (e: any) => {
console.log("Processing", e.selectedRowKeys);
setServiceTemplateIds(e.selectedRowKeys);
};
return (
<>
<Popup
visible={props.isImportVisible}
dragEnabled={false}
closeOnOutsideClick={false}
showCloseButton={false}
showTitle={true}
title="Service Template"
width={800}
height={280}>
<Position
at="center"
my="center"
/>
<DataGrid
id="serviceTemplateGrid"
key="ServiceTemplateId"
keyExpr="ServiceTemplateId"
focusedRowEnabled={true}
onSelectionChanged={onSelectionChanged}
selectedRowKeys={serviceTemplateIds}
dataSource={serviceTemplateList}
showRowLines={true}
showBorders={true}>
<Selection mode="multiple" showCheckBoxesMode="onClick" />
<Column dataField="Name" caption="Name" />
<Column dataField="CustomerName" caption="Customer Name" />
<Column dataField="BaseCurrencyCode" caption="Currency" />
<Column dataField="Description" caption="Description" />
</DataGrid>
<ToolbarItem
widget="dxButton"
toolbar="bottom"
location="before"
options={closeButtonOption}
/>
<ToolbarItem
widget="dxButton"
toolbar="bottom"
location="after"
options={importButtonOption}
/>
<div>{JSON.stringify(getServiceTemplateDetail)}</div>
</Popup>
</>
)
})
interface importServiceProp{
isImportVisible: boolean;
onCloseImportModel: Function
importingServiceTemplate: any
}
It is not React or MobX problem, just a regular javascript closure.
When you create importButtonOption function it remembers all the variables around it, and serviceTemplateDetail is equals undefined at first time. So after you invoke loadServiceTemplateDetail serviceTemplateDetail is changed in the store, but inside importButtonOption function it is still an old value which was remembered when the function was created.
Hope it makes sense. Basically you just need to read about closures and how they work.
There is a little guide in React docs
What you can do for example is remove destructuring of values and dereference them as you need, like that:
loadServiceTemplateDetail(String(serviceTemplateIds))
.then(()=>{
console.log("Callback");
console.log(JSON.stringify(serviceTemplateStore .serviceTemplateDetail)); // reference it from the store to get actual value
props.importingServiceTemplate(getServiceTemplateDetail); //Passing the imported value to parent component
});

Array.length is returning undefined when page is refreshed

I have an array that returns undefined when I check the length (array.length) only the page is refreshed.
If I open my local environment in a new tab in chrome, everything works fine, and I get the length of the array. Only after refreshing the page once, the page crashes and I get the following error:
"TypeError: Cannot read property 'length' of undefined".
I am using React, React's Context API (I am getting the array from the context provider) and Nextjs for the project. I have a feeling this issue connected with SSR, but I'm not entirely sure.
Here is the relevant code connected with the problem.
Context Provider
export function CartProvider(props) {
let initializeState;
if (typeof window !== 'undefined') {
initializeState = JSON.parse(sessionStorage.getItem('cart'));
if (initializeState === null) {
initializeState = [];
}
}
// Sets the cart
const [ cart, setCart ] = useState(initializeState);
return (
<CartContext.Provider
value={{ cart, setCart, checkCart, addItem, deleteItem, quantityIncrease, quantityDecrease }}
>
{props.children}
</CartContext.Provider>
);
}
And this is the code that is consuming the Context Provider
Cart.js
import React, { Fragment, useContext } from 'react';
import ShoppingCart from '../components/ShoppingCart';
import { CartContext } from '../contexts/CartContext';
const cart = () => {
const { cart } = useContext(CartContext);
return (
<Fragment>
{cart.length !== 0 ? (
<ShoppingCart />
) : (
<div>
<h1>Your cart is empty</h1>
</div>
)}
</Fragment>
);
};
export default cart;
Any help is greatly appreciated!
I think your bug related to SSR.
If you run the code with SSR, the initializeState will receive undefined.But initializeState should be Array as you expected.
So, I suggest you change your code:
// Change
let initializeState;
if (typeof window !== 'undefined') {
initializeState = JSON.parse(sessionStorage.getItem('cart'));
if (initializeState === null) {
initializeState = [];
}
}
// To
let initializeState = [];
if (typeof window !== 'undefined' && sessionStorage.getItem('cart')) {
initializeState = JSON.parse(sessionStorage.getItem('cart'));
}
You can make the code safer by catching the error while calling JSON.parse. But, if you make sure sessionStorage.getItem('cart') is always correct, you can keep this code.
It's a matter of asynchronicity. The variable is not initialized at the time of evaluation and that is how you get the:
"TypeError: Cannot read property 'length' of undefined"
Try applying the guard pattern when you consume the provider like so:
import React, { Fragment, useContext } from 'react';
import ShoppingCart from '../components/ShoppingCart';
import { CartContext } from '../contexts/CartContext';
const cart = () => {
const { cart } = useContext(CartContext);
return (
<Fragment>
{(cart && (cart.length !== 0)) ? (
<ShoppingCart />
) : (
<div>
<h1>Your cart is empty</h1>
</div>
)}
</Fragment>
);
};
export default cart;
Quick explainer:
cart && (cart.length !== 0)
translates to:
WHEN symbol "cart" evaluates to a truthy value,
THEN return the result of the expression "cart.length !== 0",
ELSE return false
This way you "guard" unsafe code at runtime.

React component ignore displaying text for empty obj

I have a react component with a method that reads some data from an object and then returns the text on its status on another component.
The problem is that when the application is loading at first the obj length will be 0 initially and when the object data is loaded it will then
update and the obj.length will be higher than 0.
The what happens is this:
Scenario 1 (obj will actually have data)
-- App Loading
-- getTxt checks if obj is empty
-- As data is not loaded yet on the first call obj.length will return 0 so 'No Data Found' is displayed
-- Then when the app finishes loading it then updates and the obj.length > 0 so 'Found the data' is displayed
or
Scenario 2 (obj will be empty)
-- App Loading
-- getTxt checks if obj is empty
-- As data is not loaded yet on the first call obj.length will return 0 so 'No Data Found' is displayed
-- Then when the app finishes loading it then updates and the obj is actually empty so it can stay the same.
My problem is that if after the app is loaded and obj.length re-checked it returns > then 0 then I don't want to display the first 'No Data Found',
but I need to have the condition just in case after the app has finished loading the data the data is still = 0
Here is the code:
import React from 'react'
class MyComponent extends React.Component {
getTxt() {
if (this.props.obj.length > 0) {
return 'Found the data';
} else if (this.props.obj.length == 0) {
return 'No Data Found';
}
return 'Searching Data'
}
render() {
return <SomeComponent text={this.getTxt()}/>
}
}
export {MyComponent}
What can I do in order to get this done?
As stated in the comments by Felix Kling.
You have three different states.
Initial state
No data
There is some data
Initial state will have the data props set to null. After the data was received it could be an empty object {} or an object with some information.
I would probably write this in the following way:
class MyComponent extends React.Component {
getStatus() {
const { data } = this.props;
if (data === null) {
return "Searching data"
}
if (Object.keys(data).length === 0) {
return "No data was found"
}
return "Found some data"
}
render() {
return <SomeComponent text={this.getStaus()}/>
}
}
This is almost perfect because I would like to separate logic from the view. I will have a reducer that will get the data and upon its values/length it will determine the status.
const statusReducer = (data) => {
if (data === null) {
return "Searching data"
}
if (Object.keys(data).length === 0) {
return "No data was found"
}
return "Found some data"
}
class MyComponent extends React.Component {
state = { data: null }
componentDidMount() {
getData()
.then(resp => resp.json)
.then(resp => this.setState({data: resp.data})
}
render() {
return <SomeComponent text={statusReducer(this.state.data)} />
}
}
You may ask what's the point of passing this to another function (reducer)? First, as I mentioned, separating the logic from the view. Second, statusReducer can be reused in other components. Third, easier to test the code.
NOTE: We only took care of the happy path. However, if there was a problem with the request or the response contains error, we probably get the wrong status.
To handle such case lets look at a different approach. Lets have data and status in state
class MyComponent extends React.Component {
state = { data: null, status: 'PENDING' }
componentDidMount() {
getData()
.then(resp => resp.json)
.then(resp => this.setState({data: resp.data, status: 'COMPLETE'})
.catch(err => this.setState(data: err.Message, status: 'ERROR'})
}
render() {
switch(this.state.status) {
case 'COMPLETE':
return <SomeComponent text={statusReducer(this.state.data)} />
case 'ERROR':
return <ErrorComponent message={this.state.data} />
case 'PENDING':
return <Spinner />
default:
// there is a runtime/compiling error.
// Notify it in you console.log or something
return null // Here I am silently failing but that's not a good practice
}
}
}
We could do even better if we move the switch-case statement to another function and call it loadReducer. But I will let you decide whether to do it or not.
Please notice Eitan's solution too. He uses both componentDidMount and componentWillUpdate to update status.
It sounds like you don't want to render anything until the data is received?
What I do when I need to do this is use component state to keep track of loading status. When the component mounts, set state.loading to true.
componentDidMount() {
this.setState({loading: true})
}
Set loading to false once everything is updated.
componentDidUpdate() {
if (this.state.loading) {
this.setState({loading: false})
}
}
And in the render function, conditionally render based on loading state.
render() {
const text = this.state.loading ? "Loading..." : this.getTxt()
return <SomeComponent text={text}/>
}
I agree with Felix in the comments. You may be better off with a nested ternary in a functional component. The component should update whenever props to it obj are updated.
export const myComponent = (props) => {
return (
<SomeComponent
text={ props.obj ?
props.obj.length > 0 ?
'Found Data'
:
'No Data Found'
:
'Searching'
}
/>
)
}

Categories

Resources