Infinite loop when using useLazyQuery in React + Apollo/GraphQL? - javascript

My code so far looks something like this:
const { ID } = useParams();
const [getObjects, {loading, data}] = useLazyQuery(GET_OBJECTS_BY_ID);
const objectWithID = props.data.find(datum => datum._id == ID);
if (objectWithID.conditional) {
getObjects({variables: {objectIds: objectWithID.subObjects}});
//Do a bunch of other stuff including a separate render
}
else {...}
What I'm essentially doing is finding an object with the specified ID first, and then querying for its subObjects. I want to first find the objectWithID variable before querying, and then based on one of its parameters, conditionally use its value, hence I think useLazyQuery helps to achieve this. However, this causes an infinite loop: for some reason it's being called an infinite number of times, crashing my webpage. Am I using useLazyQuery incorrectly? How could I prevent this infinite loop?

In this case, you're executing the query inside the render method, meaning it'll just keep firing. Instead, consider using a useEffect hook:
const { ID } = useParams();
const [getObjects, { loading, data }] = useLazyQuery(GET_OBJECTS_BY_ID);
const objectWithID = useMemo(() => {
return props.data.find((datum) => datum._id == ID);
}, [props.data, ID]);
useEffect(() => {
if (objectWithID.conditional) {
getObjects({ variables: { objectIds: objectWithID.subObjects } });
}
}, [getObjects, objectWithID]);
return objectWithID.conditional ? (
<>Render one thing</>
) : (
<>Render the other thing</>
);
Note that I also threw in a useMemo hook to memoize the objectWithID value so it only changes when props.data or ID changes.

Related

How do i return an object from my react state

I am trying to find an item from a collection, from the code below, in order to update my react component, the propertState object isnt empty, it contains a list which i have console logged, however I seem to get an underfined object when i console log the value returned from my findProperty function... I am trying update my localState with that value so that my component can render the right data.
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState()
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
return (<>{property.propertyName}</>)
}
export default PropertyComponent
There are a few things that you shall consider when you are finding data and updating states based on external sources of data --useParams--
I will try to explain the solution by dividing your code in small pieces
const PropertyComponent = () => {
const { propertyId } = useParams();
Piece A: Consider that useParams is a hook connected to the router, that means that you component might be reactive and will change every time that a param changes in the URL. Your param might be undefined or an string depending if the param is present in your URL
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
Piece B: useSelector is other property that will make your component reactive to changes related to that selector. Your selector might return undefined or something based on your selection logic.
const[property, setProperty] = useState()
Piece C: Your state that starts as undefined in the first render.
So far we have just discovered 3 pieces of code that might start as undefined or not.
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
Piece D: Here is where more problems start appearing, you are telling your code that in every render a function findProperty will be created and inside of it you are calling the setter of your state --setProperty--, generating an internal dependency.
I would suggest to think about the actions that you want to do in simple steps and then you can understand where each piece of code belongs to where.
Let's subdivide this last piece of code --Piece D-- but in steps, you want to:
Find something.
The find should happen if you have an array where to find and a property.
With the result I want to notify my component that something was found.
Step 1 and 2 can happen in a function defined outside of your component:
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
NOTE: I took the liberty of modify your code by simplifying a little
bit your find function.
Now we need to do the most important step, make your component react at the right time
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState({ propertyName: '' }); // I suggest to add default values to have more predictable returns in your component
/**
* Here is where the magic begins and we try to mix all of our values in a consistent way (thinking on the previous pieces and the potential "undefined" values) We need to tell react "do something when the data is ready", for that reason we will use an effect
*/
useEffect(() => {
// This effect will run every time that the dependencies --second argument-- changes, then you react afterwards.
if(propertyId, propertyState.properties) {
const propertyFound = findProperty(propertyId, propertyState.properties);
if(propertyFound){ // Only if we have a result we will update our state.
setProperty(propertyFound);
}
}
}, [propertyId, propertyState.properties])
return (<>{property.propertyName}</>)
}
export default PropertyComponent
I think that in this way your intention might be more direct, but for sure there are other ways to do this. Depending of your intentions your code should be different, for instance I have a question:
What is it the purpose of this component? If its just for getting the property you could do a derived state, a little bit more complex selector. E.G.
function propertySelectorById(id) {
return function(store) {
const allProperties = propertiesStateSelector(store);
const foundProperty = findProperty(id, allProperties);
if( foundProperty ) {
return foundProperty;
} else {
return null; // Or empty object, up to you
}
}
}
Then you can use it in any component that uses the useParam, or just create a simple hook. E.G.
function usePropertySelectorHook() {
const { propertyId } = useParams();
const property = useSelector(propertySelectorById(propertyId));
return property;
}
And afterwards you can use this in any component
functon AnyComponent() {
const property = usePropertySelectorHook();
return <div> Magic {property}</div>
}
NOTE: I didn't test all the code, I wrote it directly in the comment but I think that should work.
Like this I think that there are even more ways to solve this, but its enough for now, hope that this helped you.
do you try this:
const found = propertyState.properties.find(element => element.propertyId === propertyId);
setProperty(found);
instead of all function findProperty

Can't use updated state in function in React

I am currently using Agents.tsx as a parent function and StickyFilter.tsx as the child. I am setting a state with an initial value of an empty string in Agents.tsx like so:
const Agents = (props: AgentsProps): ReactElement => {
const { agentsMode, recruitClient, recruitGlobals, setRecruitClient, setShowLoading } = props;
const [savedSearchName, setSavedSearchName] = useState(''); // set state here
I then am passing that setSavedSearchName as a prop to StickyFilter.tsx:
const StickyFilter = (props: StickyFilterProps): ReactElement => {
const { otherProps,
setSavedSearchName
} = props;
later down in StickFilter.tsx I am calling on that state function like so:
const setSavedSearch = (savedSearch: RecruitAgentSearch): void => {
let revisedSearches: AgentSearch[] = [];
// Redacted code -- bunch of logic
// Value gets set here
setSavedSearchName(revisedSearchDetails.criteria.searchName || '');
};
Once that value gets set in StickyFilter.tsx I am able to have it console log in the parent component just fine. I have a table that renders and once I click on a certain item it fires off the saveNewNote function, but for some reason the function is still pulling in the empty string but whenever I click on the item a SECOND time, it'll pull in the right value. What am I missing here? I tried inputting a useEffect hook like so:
useEffect(() => {
console.log(savedSearchName)
}, [savedSearchName]);
but it will not console log the correct value until I do the action on the table a second time.
Any advice is appreciated.
Edit: Here is the saveNewNote function in Agents.tsx
const saveNewNote = async (agent: Agent, agentsMode: AgentsMode): Promise<void> => {
// just trying to console log here after the state is set
console.log(savedSearchName); // returning empty string
console.log(recruitStoreApi.savedSearchName);
console.log(stateApi);
// Simple logic redacted
}
I ended up changing the data structure that injects into the table. I could not find a way to get the latest state.

How can I update state in a component from a hook where that hook is used without causing infinite re-renders?

I have the following component:
const CallScreen = () => {
const [callEnded, setCallEnded] = useState(false);
const updateTimerColor = () => {
setCallEnded(true);
};
const clock = useTimer(
callStartTime,
callLength,
callTimeOut,
timerReady,
connectionState,
updateTimerColor,
);
Then in useTimer I want to call updateTimerColor when certain conditions are met. The problem is is that when the state updates, the hook is called again and because the conditions under which updateTimerColor is called are the same, the component infinitely re-renders.
How can I update state in CallScreen from the hook without creating an infinite loop?
TIA.
You should also include how you call updateTimerColor but maybe checking if callEnded changed value since last call before calling setCallEnded might prevent the infinite loop.
const updateTimerColor = () => {
//No need to set it again if the value did not change
if(!callEnded) {
setCallEnded(true);
}
};

JAVASCRIPT: Nothing was returned from render even though return statement exists

So I have this Display() function which fetches events from the Google Calendar via an API and store each event's name via element.summary into the events set. And then once the events set is populated, I iterate through the set via for (let item of events) and create a new <a> tag for each event/item in the set using the name as the text via <a>{item}</a> (for e.g. <a>call<a>, then I push each <a> tag into a new array called tabs and then finally return the tabs array. The events set contains three items and when I console.log, I see the correct items ("call", "kist", & "go") in the set. However, once I console.log the tabs array, it only contains one <a> tag whose value is null whereas it is supposed to contain three <a> tags since it iterates through the events set which has three items and is supposed to create an <a> tag for each. Also, I get the error that item is not defined for the line for (let item of events), somehow I cannot iterate through the events set. See console output here.
function Display() {
let events = new Set()
let tabs = []
ApiCalendar.listUpcomingEvents(10)
.then(({result}: any) => {
result.items.forEach(element => {
console.log(element.summary)
events.add(element.summary)
}
);
console.log(events)
for (let item of events)
console.log(item)
tabs.push(<a>{item}</a>)
console.log(tabs)
return tabs
});
}
This is the class that I made in the same file as the above function, which basically renders a 'Log In' button if user is not logged in to their calendar, or renders the array of <a> tags returned by the Display() function if user is already logged in. However, even though the Display() function above does return something (i.e. an array of <a> tags) and the render() function inside the class also returns a <div> element with the corresponding component inside the div, I get the error Uncaught Error: Display(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null. I am new to JavaScript and have no idea what I'm doing wrong. Any help is greatly appreciated and thank you in advance.
export default class LoginControl extends React.Component {
constructor(props) {
super(props);
this.state = {
sign: ApiCalendar.sign,
};
}
render() {
const isLoggedIn = this.state.sign;
let ele;
if (isLoggedIn) {
ele = <Display/>;
} else {
ele = <Button>'Sign In'</Button>;
}
return (
<div>
{ele}
</div>
);
}
}
Your Display function calls an async method and returns nothing. You will need to utilize state and effect inside Display to render returned data. But then, you will encounter errors if user navigates away from page before data is fetched.
Best solution for this problem would be to utilize redux and redux-thunk
Caution, untested code below
If you feel like you don't need redux, try this approach
async function fetchItems() {
const result = await ApiCalendar.listUpcomingEvents(10);
return result.result.items.map(({summary}) => summary);
}
function Display() {
const [items, saveItems] = useState([]);
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
(async () => {
const items = await fetchItems();
//Do not update state if component is unmounted
if (isMounted.current) {
saveItems(items);
}
})();
}, []);
return <>{items.map(item => <a key={item}>{item}</a>)}</>
}
If you want to render more than summary, you can do it like this
async function fetchItems() {
const result = await ApiCalendar.listUpcomingEvents(10);
return result.result.items.map(({summary, somethingElse}) => ({summary, somethingElse}));
//can be replaced with return [...result.result.items]; to get all props
}
function Display() {
//... Main logic of Display component is the same,
//so I wouldn't duplicate it here
return <>{items.map(item => <div key={item.summary}>{item.summary} {item.somethingElse}</div>)}</>
}
It seems that you are not returning anything on Display component.
You can't return a promise on a component so you need to make it inside useEffect using react hooks or component lifecycle - and no, you don't need redux just to achieve this.
function Display() {
let events = new Set()
let tabs = [];
const [items, setItems] = useState([]);
const getList = async () => {
const res = await ApiCalendar.listUpcomingEvents(10);
setItems(res.items);
}
useEffect(async () => {
getList();
}, []);
return items.map(item => <div>{item}</div>);
}

Looking for assistance in getting proper results from .filter()

I am trying to filter an array with a string that is input by user. The results are not updating properly with the first key input, then if the box is cleared or characters removed/changed, results that may now pass the filter are not being displayed.
The goal is to have all results displayed on initial page render, then properly updated with each keystroke.
Apologies; I'm just learning to code. Thanks for all assistance.
searchCompUsers = () => {
const newState = {}
const filteredEmps = this.props.employees.filter(
user => user.name.includes(this.state.searchName)
)
console.log(filteredEmps)
`` newState.filterEmps = filteredEmps
this.setState(newState)
}
empSearch = evt => {
const stateToChange = {};
stateToChange[evt.target.id] = evt.target.value;
this.setState(stateToChange);
this.searchCompUsers()
};
These lines are being run in sequence:
this.setState(stateToChange);
this.searchCompUsers();
...
const filteredEmps = this.props.employees.filter(
user => user.name.includes(this.state.searchName)
)
...
this.setState(newState);
I am assuming in your example, evt.target.id is searchName.
Two things you're doing here which you shouldn't do:
Running two setStates in sequence. This isn't necessarily a problem, but there's generally no reason for it and it could mean your code is structured poorly.
Referencing the state immediately after setState. setState is run asynchronously, so you can't guarantee the state will be updated by the time you reach your filter.
The weird results you're getting are probably stemming from (2).
Something like this would work better, assuming the rest of your code is fine:
empSearch = evt => {
const key = evt.target.id;
const value = evt.target.value;
if (key === "searchName") {
const filteredEmps = this.props.employees.filter(
user => user.name.includes(value);
);
this.setState({
filterEmps: filteredEmps
});
}
};
This way, you're only calling setState once per event, and you're not relying on the results of an earlier setState.
If you need to keep searchName in the state for some reason (such as using a controlled component), then you can simply add it to the same setState.
this.setState({
filterEmps: filteredEmps,
searchName: value
});
The only places you can assume the state is up-to-date is in the render() function, and in certain React lifecycle functions. You can also provide a callback to setState if necessary, though this should be relatively rare: this.setState({ ...someState }, () => { ...someCodeToRun() });

Categories