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

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;
}

Related

React renders only on first setState

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 :/

Updating an immutable variable in a child component

I have a global variable plyViewed in App.js that is set outside of my App component.
let plyViewed = 0;
function App() {
It monitors which move the board game is on. Below the board are some navigation buttons. If you click < I do plyViewed--, if you click I do plyViewed++. You get the picture.
This all worked fine, until I refactored!
I took the navigation buttons, who’s JSX code was all inside the App() function and put it in <MoveButtons /> in MoveButtons.js. So, I thought I could pass down plyViewed as a prop and then update the value in my code in the MoveButton child component. Then I find that props are immutable! Now I am stuck.
My code below gives an example of how I am using that plyViewed code. When someone clicks the navigation buttons, it fires an event that triggers the code to update plyViewed, although now it doesn’t work anymore because it is a prop. The rest of the game data is stored in an object called gameDetails.
I am passing down the plyViewed like this:
<MoveButtons
plyViewed={plyViewed}
// etc.
/>
A shortened version of my MoveList component is below.
plyViewed is used in multiple areas throughout my app, like gameDetails. I’ve considered putting it in the gameDetails object, but then I still have the issue of gameDetails being immutable if passed down as a prop. Then if I set plyViewed as a state variable, it becomes asynchronous and therefore unsuitable for use in calculations.
Am I thinking about this all wrong?
export default function MoveButtons(props) {
return (
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed > 0) {
props.plyViewed--;
props.board.current.setPosition(props.fenHistory[props.plyViewed]);
props.setFen(props.fenHistory[props.plyViewed]);
props.setSelectedIndex(props.plyViewed);
}
}}
>
<NavigateBeforeIcon />
</Button>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed < props.fenHistory.length - 1) {
props.plyViewed++;
props.board.current.setPosition(props.fenHistory[props.plyViewed]);
props.setFen(props.fenHistory[props.plyViewed]);
props.setSelectedIndex(props.plyViewed);
}
}}
>
<NavigateNextIcon />
</Button>
</Grid>
);
}
you are trying to update the props that are passed down from the higher-level component in your component tree, which is not possible.
You have the option to create a state using React's useState hook and passing down both the value and the dispatcher, but this is not recommended because you would be drilling props down the tree.
You can also pass the onClick events (or parts of them), up to your App component, which is an improvement to the first method but not the best practice in your case.
What you should really be doing is managing your global state using either, React's own Context API, or Redux. I think this could help you out.
While we're missing the full picture, it sounds like plyViewed should be a state and the asynchronous behaviour shouldn't prevent any computation if done properly with React.
It's easy to overlook the fact that the new state value is synchronously computed by ourselves when setting the state. We can just use that same local value to compute anything else and the async behaviour isn't affecting us at all.
onClick={() => {
if (props.plyViewed > 0) {
// New local value computed by ourselves synchronously.
const updatedPlyViewed = props.plyViewed - 1;
// Set the state with the new value to reflect changes on the app.
props.setPlyViewed(updatedPlyViewed);
// Use the up-to-date local value to compute anything else
props.board.current.setPosition(props.fenHistory[updatedPlyViewed]);
props.setFen(props.fenHistory[updatedPlyViewed]);
props.setSelectedIndex(updatedPlyViewed);
}
}}
This is a really simple pattern that should help solve the most basic issues with new state values.
Simple computations
Quick computations can be done in the render phase. The latest state values will always be available at this point. It's unnecessary to sync multiple state values if it can easily be computed from a single value, like the plyViewed here.
const [plyViewed, setPlyViewed] = useState(0);
// No special state or function needed to get the position value.
const position = fenHistory[plyViewed];
Here's an interactive example of how a simple state can be used to compute a lot of different derived information within the render phase.
// Get a hook function
const { useState } = React;
// This component only cares about displaying buttons, the actual logic
// is kept outside, in a parent component.
const MoveButtons = ({ onBack, onNext }) => (
<div>
<button type="button" onClick={onBack}>
Back
</button>
<button type="button" onClick={onNext}>
Next
</button>
</div>
);
const App = () => {
const [fenHistory, setFenHistory] = useState(["a", "b"]);
const [plyViewed, setPlyViewed] = useState(0);
const position = fenHistory[plyViewed];
const onBack = () => setPlyViewed((curr) => Math.max(curr - 1, 0));
const onNext = () =>
setPlyViewed((curr) => Math.min(curr + 1, fenHistory.length - 1));
return (
<div>
<p>Ply viewed: {plyViewed}</p>
<p>Fen position: {position}</p>
<p>Fen history: {fenHistory.join(", ")}</p>
<button
type="button"
onClick={() =>
setFenHistory((history) => [...history, `new-${history.length}`])
}
>
Add to fen history
</button>
<MoveButtons onBack={onBack} onNext={onNext} />
</div>
);
};
// Render it
ReactDOM.render(<App />, document.getElementById("app"));
button {
margin-right: 5px;
}
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
Expensive computations
If the computation takes a considerable amount of time to complete, and that doing it each render cycle is noticeably slowing down the rendering, there are some optimizations we could do.
useMemo will only recompute the memoized value when one of the
dependencies has changed. This optimization helps to avoid expensive
calculations on every render.
const [plyViewed, setPlyViewed] = useState(0);
const position = useMemo(() => /* costly computation here */, [plyViewed]);
Complex computations
If the computation has a lot of dependencies, we could use useReducer to manage a state object.
Note that the following example isn't justifying the use of useReducer and it's only used as an example of the implementation.
const initialState = {
plyViewed: 0,
fenHistory: ["a", "b"],
positionValue: "a",
};
function reducer(state, action) {
const { plyViewed, fenHistory } = state;
switch (action.type) {
case "back":
if (fenHistory.length <= 0) return state;
const newIndex = plyViewed - 1;
return {
...state,
plyViewed: newIndex,
positionValue: fenHistory[newIndex],
};
case "next":
if (fenHistory.length - 1 > plyViewed) return state;
const newIndex = plyViewed + 1;
return {
...state,
plyViewed: newIndex,
positionValue: fenHistory[newIndex],
};
case "add":
return {
...state,
fenHistory: [...fenHistory, action.value],
};
default:
throw new Error();
}
}
const App = () => {
const [{ plyViewed, fenHistory, positionValue }, dispatch] = useReducer(
reducer,
initialState
);
const onBack = () => dispatch({ type: "back" });
const onNext = () => dispatch({ type: "next" });
const onAdd = () => dispatch({ type: "add", value: 'anything' });
// ...
Async computations
If we need to get the result, for example, from a distant server, then we could use useEffect which will run once when the value changes.
const App = () => {
const [plyViewed, setPlyViewed] = useState(0);
const [position, setPosition] = useState(null);
useEffect(() => {
fetchPosition(plyViewed).then((newPosition) => setPosition(newPosition));
}, [plyViewed]);
There are a couple pitfalls with useEffect and asynchronously setting the state.
Prevent setting state on an unmounted component.
plyViewed may have changed again since the first fetch was triggered but before it actually succeeded, resulting in a race-condition
Then if I set plyViewed as a state variable, it becomes asynchronous and therefore unsuitable for use in calculations.
I think this is incorrect, you just need to start using useEffect as well:
function App() {
const [plyViewed, setPlyViewed] = useState(0);
<MoveButtons
plyViewed={plyViewed}
setPlyViewed={setPlyViewed}
/>
}
export default function MoveButtons(props) {
useEffect(() => {
props.board.current.setPosition(props.fenHistory[props.plyViewed]);
props.setFen(props.fenHistory[props.plyViewed]);
props.setSelectedIndex(props.plyViewed);
}, [props.plyViewed, props.fenHistory]);
return (
<Grid item xs={6}>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed > 0) {
props.plyViewed--;
}
}}
>
<NavigateBeforeIcon />
</Button>
<Button
variant="contained"
color="primary"
size="large"
style={{ maxWidth: props.buttonWidth, minWidth: props.buttonWidth }}
onClick={() => {
if (props.plyViewed < props.fenHistory.length - 1) {
props.plyViewed++;
}}
>
<NavigateNextIcon />
</Button>
</Grid>
);

use componentWillMount without a class

with react-native, I want to use componentWillMount without using a class
await Font.loadAsync({
gotham_medium: require("../../assets/GothamMedium_1.ttf")
});
}
const Button = (props: TouchableOpacityProps & ButtonProps) => (
<TouchableOpacity {...props} style={styles.button}>
<Text style={styles.title}>{props.title}</Text>
</TouchableOpacity>
);
export default Button;
But I have a problem on the device :
error on the device
It says the problem is on this line (and it is):
async componentWillMount = () => {
When you use an async function, the async keyword goes right before () => (a vanilla js syntax error). Like this:
componentWillMount = async () => {
But, that's not the main problem. When not using a class, you need the useEffect hook.
So, try something like this (the whole component, and deleting componentWillMount):
const Button = (props: TouchableOpacityProps & ButtonProps) => {
useEffect(async () => {
await Font.loadAsync({
gotham_medium: require("../../assets/GothamMedium_1.ttf")
});
}, []);
return (
<TouchableOpacity {...props} style={styles.button}>
<Text style={styles.title}>{props.title}</Text>
</TouchableOpacity>
);
};
And at the top of the file:
import { useEffect } from 'react';
You can use Hooks for this,
from the docs,
If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
And
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
useEffect(async () => {
await Font.loadAsync({
gotham_medium: require("../../assets/GothamMedium_1.ttf")
});
},[]);

How to avoid using setState in render method?

I am using react-apollo to fetch data through <Query /> and <Mutation />.
Thus, I want to setState when I get some data. I am getting the data in the render method.
Like this:
render() {
return (
<Query query={CAN_UPDATE_POST_QUERY} variables={{ id: this.props.router.query.postId }}>
{ payload => {
if(payload.loading) {
<div style={{width: '98%', textAlign: 'center', maxWidth: '1000px', margin: '50px auto'}}>Loading...</div>
}
if(this.isNew()){
return (
<PleaseSignIn>
{ me => (
...something
) }
</PleaseSignIn>
)
} else if (payload.data && payload.data.canUpdatePost) {
// I get payload here. Here's where I want to set new state.
this.canUpdatePost = payload.data.canUpdatePost
this.setState({ canUpdatePost: this.canUpdatePost })
return (
<PleaseSignIn>
{ me => (
...something
) }
</PleaseSignIn>
)
} else {
return (
<div style={{width: '98%', textAlign: 'center', maxWidth: '1000px', margin: '50px auto'}}>You and your mind seems to be lost. 🐡</div>
)
}
} }
</Query>
)
}
Using setState in render gives me this error:
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
How do I think in React way? And especially, how do I get my state changed when I get payload from react-apollo?
NEWBIE HERE. Sorry if silly.
Thanks in advance. :-)
In general, you should avoid using setState in your render functions. You should avoid having side affects (such as setting state) in your render function and instead should call other component functions to handle data change in your component.
The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it’s invoked, and it does not directly interact with the browser.
See the render() method reference in the react docs: https://reactjs.org/docs/react-component.html#render
You can create a function outside of your render method to fix this problem like so:
YourComponent extends React.Component {
handleCanUpdatePost = (canUpdatePos) => {
this.setState({ canUpdatePost })
}
render() {
// Your render logic here
// Whenever you want to setState do so by
// calling this.handleCanUpdatePost(payload.data.canUpdatePost)
}
}
You should also probably have a check to see if the value of state is going to change before setting it to avoid unnecessary re-renders:
handleCanUpdatePost = (canUpdatePos) => {
this.setState((state) => {
if(canUpdatePos !== state.canUpdatePost) {
return {canUpdatePost: payload.data.canUpdatePost}
}
})
}

this.props.children.map at react native is not working

I want to render a child component from a parent component by passing to it one object from array of objects fetched from an api.
TypeError: this.props.posts.map is not a function
renderPosts() {
return this.props.posts.map(post =>
<HomeCard key={post.id} postData={post} />
);
}
All the component:
class Home extends Component {
componentWillMount() {
this.props.getUserPosts();
}
renderPosts() {
return this.props.posts.map(post =>
<HomeCard key={post.id} postData={post} />
);
}
render() {
return (
<View>
<View style={{ paddingBottom: 55 }}>
<SearchBar />
</View>
<ScrollView>
{this.renderPosts()}
</ScrollView>
</View>
);
}
}
const mapStateToProps = state => {
const posts = state.homePost;
console.log('posts', posts);
return { posts };
};
export default connect(mapStateToProps, { getUserPosts })(Home);
I suspect this is because this.props.posts is undefined (empty or whatever default you have it set to) when Home being mounted. Since you aren't giving us any log outputs, it's hard to tell but this is a very common mistake.
The immediate fix is to give it a default value either where you define your initial state for your reducer or in mapStateToProps. The latter looking something like this (adapting your code):
const mapStateToProps = state => {
const posts = state.homePost || [];
console.log('posts', posts);
return { posts };
};
While this will fix your error, another thing you need to correct is the common misconception that whatever is in componentWillMount will execute prior to mounting. This is not true and is one of the reasons that this lifecycle method (and componentWillReceiveProps and componentWillUpdate) will be deprecated in the future.
Your code here:
componentWillMount() {
this.props.getUserPosts();
}
is asynchronous since you mention fetching this data. getUserPosts will fire but isn't guaranteed to complete before mounting. So while you think this.props.posts will be set to some value before rendering, that is not going to be the case. Hence why you are getting the not a function error message.

Categories

Resources