Unable to display promise data inside a method in render - reactjs

I realize I can't return objects in a react render but I'm having a problem figuring out how to do this.
Error message:
Invariant violation, objects are not valid as a React child error
My method:
displayListItemComponent = item => {
return this.getConcernsDB(item.values)
.then(returnedConcerns => {
if(returnedConcerns.length) {
returnedConcerns.forEach(el => {
return(el.name);
})
}
}
)
}
Partial render:
<CollapseBody>
{ el.details.map(item => (
this.displayListItemComponent(item)
))}
</CollapseBody>
Here I am trying to return el.name from displayListItemComponent
I am attempting to return displayListItemComponent in my react render.
My assumption was that by returning the function this.getConcernsDB that it would chain down and it would return el.name. How can I achieve this?

Your method returns a Promise. Render should be synchronous and have no side effects (e.g. it should only return allowed values from props and state). Therefore you need to store your data in state, and render elements from state

You could do something like this. Also what does el.name contain?
displayListItemComponent =(props) => {
const {item} = props;
const [data, setData] = useState();
useEffect(() => {
// i see you did this.getConcernsDB. where is it coming from?
getConcernsDB(item.values)
.then(returnedConcerns => {
setData(returnedConcerns.values(el => el.name))
)
}, [])
if(!data) return <SomeLoadingSpinner />
// now you have access to the names in `data` variable. Return whatever component you want using the `data`
}

Related

ReactJS context-api won't render after I get data

This is a next.js site, since both my Navbar component and my cart page should have access to my cart's content I created a context for them. If I try to render the page, I get:
Unhandled Runtime Error
TypeError: Cannot read properties of undefined (reading 'key')
obs: The cartContent array exists and has length 1, I can get it by delaying when the data's rendered by using setTimeout, but, can't get it to render right after it's fetched.
I need to make it render after the data from firebase is returned, but always met with the mentioned error.
This is my _app.tsx file
function MyApp({ Component, pageProps }) {
// set user for context
const userContext = startContext();
return (
<UserContext.Provider value = { userContext }>
<Navbar />
<Component {...pageProps} />
<Toaster />
</UserContext.Provider>
);
}
export default MyApp
This file has the startContext function that returns the context so it can be used.
export const startContext = () => {
const [user] = useAuthState(auth);
const [cart, setCart] = useState(null);
const [cartContent, setCartContent] = useState(null);
useEffect(() => {
if (!user) {
setCart(null);
setCartContent(null);
}
else {
getCart(user, setCart, setCartContent);
}
}, [user]);
return { user, cart, setCart, cartContent, setCartContent };
}
This file contains the getCart function.
export const getCart = async (user, setCart, setCartContent) => {
if (user) {
try {
let new_cart = await (await getDoc(doc(firestore, 'carts', user.uid))).data();
if (new_cart) {
let new_cartContent = []
await Object.keys(new_cart).map(async (key) => {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
});
console.log(new_cartContent);
setCartContent(new_cartContent);
console.log(new_cartContent);
setCart(new_cart);
}
else {
setCart(null);
setCartContent(null);
}
} catch (err) {
console.error(err);
}
}
}
This is the cart.tsx webpage. When I load it I get the mentioned error.
export default () => {
const { user, cart, cartContent } = useContext(UserContext);
return (
<AuthCheck>
<div className="grid grid-cols-1 gap-4">
{cartContent && cartContent[0].key}
</div>
</AuthCheck>
)
}
I've tried to render the cart's content[0].key in many different ways, but couldn't do it. Always get error as if it were undefined. Doing a setTimeout hack works, but, I really wanted to solve this in a decent manner so it's at least error proof in the sense of not depending on firebase's response time/internet latency.
Edit:
Since it works with setTimeout, it feels like a race condition where if setCartContent is used, it triggers the rerender but setCartContent can't finish before stuff is rendered so it will consider the state cartContent as undefined and won't trigger again later.
Try changing
{cartContent && cartContent[0].key}
to
{cartContent?.length > 0 && cartContent[0].key}
Edit:: The actual problem is in getCart function in line
let new_cart = await (await getDoc(doc(firestore, 'carts', user.uid))).data();
This is either set to an empty array or an empty object. So try changing your if (new_cart) condition to
if (Object.keys(new_cart).length > 0) {
Now you wont get the undefined error
Since there seemed to be a race condition, I figured the setCartContent was executing before its content was fetched. So I changed in the getCart function the map loop with an async function for a for loop
await Object.keys(new_cart).map(async (key) => {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
});
to
for (const key of Object.keys(new_cart)) {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
}
I can't make a map function with await in it without making it asynchronous so I the for loop made it work. Hope someone finds some alternatives to solving this, I could only come up with a for loop so the code is synchronous.

How to force component render in React

I am making a simple ajax call to an API that produces data every 5 seconds, the data is received correctly using observables, and the data state is updated as expected.
the problem is: the page is rendering only once when it is loaded, and not when date is changed using the setData function. if I reload the page by navigating to another link and then come back the data shows as expected
her is the code:
function MyComponent() {
const [data, setData] = useState(RxJSStore.getData())
useEffect(() => {
RxJSStore.addChangeListener(onChange);
if (data.length === 0) loadDataRxJSAction()
return () => RxJSStore.removeChangeListener(onChange)
}, [data.length])
function onChange() {
setData(RxJSStore.getData())
}
return (
<>
<List data={data} />
</>
)
}
There is a way around this by forcing the component to render every time the onChange method is called. It is written in the documentation that this must only be used for testing purposes, but as the situation does not have another solution this might be the solution for this use case.
import { useRefresh } from 'react-tidy'
function MyComponent() {
const [data, setData] = useState(RxJSStore.getData())
const refresh = useRefresh()
useEffect(() => {
RxJSStore.addChangeListener(onChange);
if (data.length === 0) loadDataRxJSAction()
return () => RxJSStore.removeChangeListener(onChange)
}, [data.length])
function onChange() {
setWarnings(RxJSStore.getWarnings())
refresh()
}
return (
<>
<List data={data} />
</>
)
}

Warning for render method that should be a pure function of props and state

I have a component that I render when there is an alert that's pulled from the state. Currently the alert dispatches and displays but i'm getting the following error and i'm not sure how I would rework this component to comply.
Error:
Warning: Render methods should be a pure function of props and state; triggering nested component updates from render is not allowed. If necessary, trigger nested updates in componentDidUpdate.
const Alerts = ({ alerts }) =>
alerts !== null &&
alerts.length > 0 &&
alerts.map((alert) =>
notification[`${alert.type}`]({
key: `${alert.id}`,
message: `${alert.title}`,
description: `${alert.message}`,
})
);
Indeed the render function should be pure and without side-effects. Remember, the entire function body of a functional component is the render function. Use an useEffect hook to issue side-effects.
Also, instead of using array::map, use array::forEach as that is the intended array function to issue side-effect on an array.
Remember to explicitly return null from the function to indicate to react there is nothing to render.
const Alerts = ({ alerts }) => {
useEffect(() => {
if (alerts) {
alerts.forEach(alert => {
notification[alert.type]({
key: alert.id,
message: alert.title,
description: alert.message,
})
})
}
}, [alerts]);
return null;
};
I think you are not able to return an array of ReactElements that way, try this:
const Alerts = ({ alerts }) => {
if(alerts !== null &&
alerts.length > 0) {
return
(<>
{alerts.map(({type, id, title, message}) =>
notification[type]({
key: `${id}`,
message: `${title}`,
description: `${message}`,
})}
</>)} else { return null }
});

What's the React best practice for getting data that will be used for page render?

I need to get data that will be used for the page that I'm rendering. I'm currently getting the data in a useEffect hook. I don't think all the data has been loaded before the data is being used in the render. It's giving me an error "property lastName of undefined" when I try to use it in the Chip label.
I'm not sure where or how I should be handling the collection of the data since it's going to be used all throughout the page being rendered. Should I collect the data outside the App function?
const App = (props) => {
const [teams] = useState(["3800", "0200", "0325", "0610", "0750", "0810"]);
const [players, setPlayers] = useState([]);
useEffect(() => {
teams.forEach(teamId => {
axios.defaults.headers.common['Authorization'] = authKey;
axios.get(endPoints.roster + teamId)
.then((response) => {
let teamPlayers = response.data.teamPlayers;
teamPlayers.forEach(newPlayer => {
setPlayers(players => [...players, newPlayer]);
})
})
.catch((error) => {
console.log(error);
})
});
}, []);
let numPlayersNode =
<Chip
variant="outlined"
size="small"
label={players[1].lastName}
/>
return (...
You iterate over a teamPlayers array and add them one at a time, updating state each time, but players is always the same so you don't actually add them to state other than the last newPlayer.
Convert
teamPlayers.forEach(newPlayer => {
setPlayers(players => [...players, newPlayer]);
});
to
setPlayers(prevPlayers => [...prevPlayers, ...teamPlayers]);
Adds all new players to the previous list of players using a functional state update.
You also have an initial state of an empty array ([]), so on the first render you won't have any data to access. You can use a truthy check (or guard pattern) to protect against access ... of undefined... errors.
let numPlayersNode =
players[1] ? <Chip
variant="outlined"
size="small"
label={players[1].lastName}
/> : null
You should always create a null check or loading before rendering stuff. because initially that key does not exists. For example
<Chip
variant="outlined"
size="small"
label={players.length > 0 && players[1].lastName}
/>
this is an example of a null check
For loading create a loading state.
When functional component is rendered first, useEffect is executed only after function is returned.
and then, if the state is changed inside of useEffect1, the component will be rendered again. Here is a example
import React, {useEffect, useState} from 'react'
const A = () => {
const [list, setList] = useState([]);
useEffect(() => {
console.log('useEffect');
setList([{a : 1}, {a : 2}]);
}, []);
return (() => {
console.log('return')
return (
<div>
{list[0]?.a}
</div>
)
})()
}
export default A;
if this component is rendered, what happen on the console?
As you can see, the component is rendered before the state is initialized.
In your case, error is happened because players[1] is undefined at first render.
the simple way to fix error, just add null check or optional chaining like players[1]?.lastName.

FlatList not updating with React Hooks and Realm

I am writing a custom hook to use it with realm-js.
export default function useRealmResultsHook<T>(query, args): Array<T> {
const [data, setData] = useState([]);
useEffect(
() => {
function handleChange(newData: Array<T>) {
// This does not update FlatList, but setData([...newData]) does
setData(newData);
}
const dataQuery = args ? query(...args) : query();
dataQuery.addListener(handleChange);
return () => {
dataQuery.removeAllListeners();
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[query, ...args]
);
return data;
}
In my component:
const MyComponent = (props: Props) => {
const data = useRealmResultsHook(getDataByType, [props.type]);
return (
<View>
<Text>{data.length}</Text>
<FlatList
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
/>
</View>
);
};
In the previous component, when doing setData(newData), the data.length gets updated correctly inside the Text. However, the FlatList does not re-render, like the data did not change.
I used a HOC before and a render prop with same the behavior and it was working as expected. Am I doing something wrong? I'd like to avoid cloning data setData([...newData]); because that can be a big amount of it.
Edit 1
Repo to reproduce it
https://github.com/ferrannp/realm-react-native-hooks-stackoverflow
The initial data variable and the newData arg in the handler are the links to the same collection. So they are equal and setData(newData) won’t trigger component’s re-render in this case.
It might be helpful to map Realm collection to the array of items’ ids. So, you will always have the new array in the React state and render will occur properly. It's also useful to check only deletions and insertions of the collection to avoid extra re-renders of the list. But in this case, you should also add listeners to the items.
function useRealmResultsHook(collection) {
const [data, setData] = useState([]);
useEffect(
() => {
function handleChange(newCollection, changes) {
if (changes.insertions.length > 0 || changes.deletions.length > 0) {
setData(newCollection.map(item => item.id));
}
}
collection.addListener(handleChange);
return () => collection.removeListener(handleChange);
},
[]
);
return data;
}

Resources