React renders only on first setState - javascript

I am trying to use chat-ui-kit-react to populate as MessageList with messages I get from another function:
Component to render Chat:
export default function Chat() {
const msgListRef = useRef();
const [history, setHistory] = useState([])
function handleHistory(response) {
if (!response?.length) {
return;
}
if (response.length > history?.length) {
msgListRef.current.scrollToBottom('smooth');
}
setHistory(response);
}
useEffect(() => {
handleHistory(getTalkHistory().talkhistory)
setInterval(() => {
handleHistory(getTalkHistory().talkhistory)
}, 5000)
}, [])
return <main>
<MessageList style={{ display: "flex" }} ref={ msgListRef }>
<Message model={{
message: 'Hello!',
position: 'single',
direction: 'incoming',
}} />
{ history?.length ? <MessageSeparator content={history[0].time.toLocaleString()} /> : null }
{ history.map(message => <Message model={{
message: converter.makeHtml(message.text),
position: 'single',
direction: message.direction,
}} /> )}
{hljs.highlightAll()}
</MessageList>
</main>
On load it will display the "Hello!" and the first time getTalkHistory returns something that is not empty (ie after it has been populated through other means), it will re-render and correctly display any messages sent/received by the time setState is called. If I increase the interval time higher, it will display more messages (if any have been sent or received). It does not matter how long the app has been running before getTalkHistory returns something for the first time, it will always work the first time, but not after.
However, afterwards it never rerenders again. No matter how many messages are being added and returned by getTalkHistory, it never updates again :/
getTalkHistory returns Chatdata as an array. If I console.log it is correctly updated at all times:
If I console.log the history.map it looks like this on the first time it renders:
However the console.log only fires two times, one time when getTalkHistory first returns something and then on the next interval (ie 5 seconds later). The second console.log can show additional messages that have been generated since, but hose messages are never rendered and the map console.log never fires again afterwards. console.log(getTalkHistory()) will fire correctly inside the setInterval and always return all messages though. So the state keeps getting updated, it just doesn't render :/

Related

React not rendering element if async call for data takes too long

I'm grabbing user data from an API. The API call takes a while. But even when the frontend gets the response, React never finishes rendering the component.
I can replicate this behavior with setTimeout.
A small timeout like 7ms will work, but if I use a larger timeout like 700ms <h1>Text rendered!</h1> doesn't display.
useEffect(() => {
setTimeout(() => setUsers({ data: ["user1", "user2", "user3"] }), 700);
}, []);
In the debugger I can see that the flow seems to be working
The frontend receives the response, and calls setUser.
This triggers the second useEffect callback which updates users.
This re-renders the component, and users && users.updated is true, but then <h1>Text rendered!</h1> doesn't render for some reason.
Oddly though, on codesandbox, I get completely different behavior: <h1>Text rendered!</h1> never renders, even if I use a small timeout like 1ms, or even if I just use setUsers with no setTimeout. And on my local environment, sometimes 700ms does work if I step through it in the debugger slowly but I can't always replicate this.
What's going on? How can I get {users && users.updated && <h1>Text rendered!</h1>} to render properly on longer asynchronous calls?
Full code:
import React, { useEffect, useState } from "react";
export default function App() {
const [users, setUsers] = useState(null);
// 1. Get Data from API
useEffect(() => {
//----replicating asyncronous API call----
//"Text rendered!" doesn't render if the timeout is large (700ms)
//but does render if timeout is small (2ms)
//(On codesandbox "Text rendered!" never renders, even if you don't use a timeout just use setUsers)
setTimeout(() => setUsers({ data: ["user1", "user2", "user3"] }), 2);
}, []);
// 2. Once Data is Receive from API, Transform It
useEffect(() => {
if (users) {
const usersCopy = users;
usersCopy.updated = true;
setUsers(usersCopy);
}
}, [users]);
// 3. After the Data is Transformed, Render "Text Rendered!"
return (
<>
Render text below:
{users && users.updated && <h1>Text rendered!</h1>}
</>
);
}

How to re-render a Button to change "disabled" property on "renderMessageImage" with react-native-gifted-chat?

I am learning React Native and this is my first post at Stack Overflow.
I'm trying to re-render a button to make its disabled property change from false to true when onPress.
This button is being manually rendered insiderenderMessageImage from react-native-gifted-chat.
I am doing it through a boolean in this.state, however I see the state value updating on the logs but the button remains visually the same and it can be pressed again and again without actually changing its disabled property.
I have in my Chat.js class:
constructor(props) {
super(props);
this.state = {
isButtonDisabled: false
};
}
Then from Gifted Chat I call renderMessageImage to show a custom message design when a message has an image:
renderMessageImage = (props) => {
return (
<View>
<Button
title="Disable Button"
disabled={this.state.isButtonDisabled}
onPress={() => this.disableButton(props.currentMessage.id)}
/>
</View>)
}
This custom design is just a button that should call another method and then disable self:
disableButton = async (message_id) => {
console.log("Disabling message_id: " + message_id); //This shows the msg_id correctly where the button is
console.log(this.state.isButtonDisabled); //This shows false
this.setState(previousState => ({
isButtonDisabled: true
}));
console.log(this.state.isButtonDisabled); //This shows true (should update button disabled property)
return true;
}
For what I have tested so far, I can see that state value of isButtonDisabled is correctly changing from false to true, and as I have read, a change in the state should make the component re-render, but unfortunately it is not working that way.
More in-depth testing:
I then headed to GiftedChat.js sources from the react-native-gifted-chat to try debugging some of the code and see what is going on there.
What I found is that whenever I press my custom button, the componentDidUpdate of GiftedChat.js is being called twice:
componentDidUpdate(prevProps = {}) {
const { messages, text, inverted } = this.props;
console.log(this.props !== prevProps); //This is called twice per button click
if (this.props !== prevProps) {
//this.setMessages(messages || []); //I changed this line for the line below to directly update state
this.setState({ messages });
console.log(this.props !== prevProps); //This is called once per button click (first of the two calls)
}
if (inverted === false &&
messages &&
prevProps.messages &&
messages.length !== prevProps.messages.length) {
setTimeout(() => this.scrollToBottom(false), 200);
}
if (text !== prevProps.text) {
this.setTextFromProp(text);
}
}
Once all this checked, I see that the state is being updated and the GiftedChat.js component and messages are being updated once per button click, however my button in renderMessageImage is never re-rendering to properly show its new disabled value and actually become disabled.
I am totally clueless what else to test, so I would really appreciate some help on what am I doing wrong.
Thank you very much in advance.
Edit: renderMessageImage is called by GiftedChat on my Chat.js class render() section, then GiftedChat will call this.renderMessageImage whenever a message has an image (eg. this.state.messages[i].image != undefined). That part is working correctly, everything is being called as it should, just the re-render of the button status (disabled) is not updating:
render() {
return (
<View style={{ flex: 1 }}>
<GiftedChat
messages={this.state.messages}
onSend={this.sendMessage}
user={this.user}
placeholder={"Write a message..."}
renderMessageImage={this.renderMessageImage}
/>
<KeyboardAvoidingView behavior={'padding'} keyboardVerticalOffset={80}/>
</View>
);
}

How do I prevent a function from being run before data is loaded into this.state (i.e. in componentDidMount)?

I have a function getRepresentativeName() which uses a piece of data (this.state.communityData.Representative) from this.state to index into a database and retrieve a string. My intent is that when the component mounts, all of the information in this.state, including this.state.communityData.Representative, will be populated and the representative's name will appear in the location set off by {...}. However, the function runs immediately when the first render is called and does not wait for the component to mount; as a result, since this.state is not populated yet, the getRepresentativeName function crashes.
I placed a ternary statement to check whether the communityData JSON object has been filled (the logic being that if it is not filled, i.e. === {}, just return null instead of executing getRepresenativeName()); Interestingly enough, the page crashes regardless (with the same issue: getRepresenativeName() is being called before this.state is populated in componentDidMount()).
Can someone explain to me why this is happening, and how I could resolve this situation without adding redundant data to this.state? I'm not sure if this is the way to solve it, but I thought that the problem could be solved if there was a way to make the getRepresentativeName() function wait until componentDidMount() finishes. Thank you!
Note: If I do not call the function and just replace the {...} with {this.state.communityData.Representative}, which fills the element with the Representative ID as opposed to the name, the page renders perfectly. In addition, I tried making getRepresentative name a callback, and that still didn't work (i.e. the page still crashed when it tried to render).
async getRepresentativeName() {
const representativeSnapshot = await database()
.ref('users')
.child(this.state.communityData.Representative)
.child('name')
.once('value');
return representativeSnapshot.val();
};
render() {
console.log('starting render');
return (
<View style={styles.containerScreen} testID="communityScreen">
{/*
This the container that holds the community Image
and its description,
*/}
<View style={styles.containerhorizontal}>
<View style={styles.containervertical}>
<Image
source={require('../../sample_images/va10.jpg')}
style={styles.image}
/>
</View>
<View style={styles.containervertical}>
<Text style={styles.containerhorizontal}>
Description: {this.state.communityData.Description}
</Text>
<Text style={styles.containerhorizontal}>
Representative: **{this.state.communityData !== {} ?this.getRepresentativeName() : null}**
</Text>
...
The typical pattern here is initialize your state with some kind of loading property. Then, asynchronously load data based on input from the user interface. That input could be from the user or it could be from lifecycle events. In this case, componentDidMount. Your component should know how to render when loading is true and when data is available.
class MyComponent extends React.Component {
state = {loading: true, error: null, representative: null}
componentDidMount() {
fetchData().then((representative) => {
this.setState({ loading: false, representative })
}).catch((error) => {
this.setState({ loading: false, error: error.message })
})
}
render() {
return this.state.error
? <span>woops</span>
: this.state.loading
? <span>busy</span>
: <div>Representative: {this.state.representative.name}</div>
}
}

useEffect not being called and not updating state when api is fetched

I'm fetching data from a weather api using useEffect hook and declaring the dependency correctly as well. My state is still not being updated and I get errors in my render function because of that. I've pretty much tried everything from getting rid of the dependency array to declaring multiples in the dependency array. I don't know what's wrong with my function. The API's JSON response is in this format:
{
location: {
name: "Paris",
region: "Ile-de-France",
},
current: {
last_updated_epoch: 1564279222,
last_updated: "2019-07-28 04:00",
temp_c: 16,
temp_f: 60.8,
is_day: 0,
condition: {
text: "Clear",
icon: "//cdn.apixu.com/weather/64x64/night/113.png",
code: 1000
},
wind_mph: 6.9,
wind_kph: 11.2
}
}
and this is what my code looks like:
const Weather = ({ capital }) => {
const [weather, setWeather] = useState(null);
useEffect(() => {
console.log("useEffect called");
const getWeather = async () => {
try {
const res = await axios.get(
`http://api.apixu.com/v1/current.json?key=53d601eb03d1412c9c004840192807&q=${capital}`
);
setWeather(res.data);
} catch (e) {
console.log(e);
}
};
getWeather();
}, [capital]);
console.log(weather);
return (
<Card style={{ width: "18rem", marginTop: "25px" }}>
<Card.Img variant="top" src={weather.current.condition.icon} />
<Card.Header style={{ textAlign: "center", fontSize: "25px" }}>
Weather in {capital}
</Card.Header>
</Card>
)
}
I expect to get to be shown image of the icon but I get this error message in my render function:
TypeError: Cannot read property 'current' of null
Weather
src/components/Weather.js:26
23 |
24 | return (
25 | <Card style={{ width: "18rem", marginTop: "25px" }}>
26 | <Card.Img variant="top" src={weather.current.condition.icon} />
| ^ 27 |
28 | <Card.Header style={{ textAlign: "center", fontSize: "25px" }}>
29 | Weather in {capital}
and my console.log(weather) return null, the original state even though its being called after useEffect() and console.log(useEffect called) does not log at all which mean useEffect is not being called.
The error message gives it away, Cannot read property 'current' of null, the only place where current is called is in weather.current in the src of Card.Img, so we deduce that weather was null during the render.
The reason this happens is because the api call is asynchronus, it doesn't populate the state immediately, so the render happens first and tries to read .current from the initial weather state null.
Solution: in your render method, make sure not to read weather.current while weather is null.
You can for example use {weather && <Card>...</Card} to hide the whole card until the data is loaded and show a loading indicator, or you can use src={weather && weather.current.condition.icon} as a quick workaround.
const Weather = ({capital}) => {
const [weather, setWeather] = useState(null);
useEffect(() => {
console.log("useEffect called");
const getWeather = async () => {
try {
const res = await axios.get(
`http://api.apixu.com/v1/current.json?key=53d601eb03d1412c9c004840192807&q=${capital}`,
);
setWeather(res.data);
} catch (e) {
console.log(e);
}
};
getWeather();
}, [capital]);
console.log(weather);
return (
<Card style={{width: "18rem", marginTop: "25px"}}>
<Card.Img variant="top" src={weather && weather.current.condition.icon} />
<Card.Header style={{textAlign: "center", fontSize: "25px"}}>
Weather in {capital}
</Card.Header>
</Card>
);
};
I had the same puzzling issue one time
You can try adding a key prop on the component when it is created in the parent code
<yourcomponent key="some_unique_value" />
This is because in most cases, when your component is reused, based on the way it is created, it may usually re-render it with some changes instead of creating it again when you reuse it, Hence the useEffect is not going to be called. eg in SwitchRoute, loops, conditionals...
So adding a key will prevent this from happening. If it is in a loop, you need to make sure each element is unique, maybe by including the index i in the key if you can't find any better unique key.
So, I was facing a similar issue, where it seemed like useEffect hook was not getting invoked at all. The concerned code snippet is given below:
Within my functional component, this was the sequence of the appearance of the code:
a variable const facetFields = []; declaration which was supposed to be set by a AJAX call from within useEffect hook
useEffect hook within which the above variable was getting set with AJAX.
useEffect( ()=>{
console.log('FacetTreeView: useEffect entered');
facetFields = getFacetFieldNames(props.tabIndex);
}, []);
JSX code that uses the variable.
return (<MyComponent> { facetFields.map((facetLabel, index) => {
populateFacetInstances(facetLabel) }) } </Component>)
With this code, the console statement inside the hook was not printing at all. Also, I kept getting a undefined error while map'ing the facetFields. So, it turns out that the useHook is called after the component is rendered. So, during the rendering, the variable facetFields is undefined.So, I fixed this by adding this line to my JSX rendering part of code:
facetFields && facetFields.map(.....
Once, I made the above change, the control started going to the hook.So, in summary, if there is any code within JSX that is being set from useEffect hook, then its best to defer the execution of the JSX till hook execution is completed. Although, this is not mandatory, but it will make the JSX code easier to read. This can be achieved in a simple way by using a boolean state flag.
Define the state:
const [readyForRender, setReadyForRender] = React.useState(false);
2. Set state in hook.
useEffect( ()=>{
console.log('FacetTreeView: useEffect entered');
facetFields = getFacetFieldNames(props.tabIndex);
setReadyForRender(true); }, []);
Render JSX conditionally.
if(readyForRender){
return ( { facetFields.map((facetLabel, index) => { populateFacetInstances(facetLabel) }) } );
} else{
return null;
}

How can I avoid componentDidCatch() from firing something multiple times?

If a component in rendered 10 times, and has an error, componentDidCatch() will fire 10 times with that same error. Currently I make an API request on catch to log the error, but I only want that error logged once.
My first though was to save my own prevError in state, and check if the error passed to componentDidCatch() was the same. But that won't work since the setState request isn't immediate. Some other lifecycle events are passed the latest state but this isn't. I know setState() takes a callback with up-to-date state but by then the error will always be equal to the prevError. Here's what I mean:
componentDidCatch(error, info) {
this.setState({ error: true });
const isNewError = (error.toString() !== this.state.prevError.toString());
// always true since setState is async
if (isNewError) {
logErrorToMyService(error, info); // should only run once
this.setState({ prevError: error });
}
}
I also don't think I can use componentDidUpdate() somehow because that doesn't know my error.
Am I missing something? Am I just handling this problem wrong and need to rework it (maybe move the check into the logErrorToMyService instead)?
A full React example of what I mean:
const logErrorToMyService = () => console.warn('I should only be run once');
class ErrorBoundary extends React.Component {
state = {
error: false,
prevError: new Error(),
}
componentDidCatch(error, info) {
this.setState({ error: true });
// none of the below code will work correctly
const isNewError = (error.toString() !== this.state.prevError.toString());
// always true since setState is async
if (isNewError) {
logErrorToMyService(error, info); // should only run once
this.setState({ prevError: error });
}
}
render() {
if (this.state.error) {
return <div>Something went (purposefully) wrong.</div>;
}
return this.props.children;
}
}
// below is 10 purposefully buggy component instances
const BuggyComponent = () => (
<div>{purposefully.throwing.error}</div>
)
const App = () => (
<ErrorBoundary>
{[...Array(10)].map((_, i) => <BuggyComponent key={i} />)}
</ErrorBoundary>
)
ReactDOM.render(
<App />,
document.getElementById('root')
);
<div id='root'></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.3.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.3.1/umd/react-dom.development.js"></script>
Update: Along with switching prevError to a field on the class, I also changed it to an array so that repeat errors - but that might have a different error in between - are also skipped.
In the code you posted, componentDidCatch() will fire a dozen or so times in quick succession long before the setstate callback runs for the first time. However prevError doesn't have to be part of state, given that is has no bearing on the appearance of the component.
Just implement it as class variable instead so it is set synchronously.

Categories

Resources