I'm trying to update the state 'allMovieList' to render a list of movies.
The idea was to set a dynamic URL in my GET request, by updating the 'page' state when clicking on the button. Unfortunately this doesn't trigger a re-rendering since the request is made only one time in componentDidMount() method.
state = {
allMovieList: [],
page: 1
}
componentDidMount() {
this.changePage();
}
async changePage() {
try {
const response = await axios.get(`https://api.themoviedb.org/4/discover/movie?api_key=${apiKey}&page=${this.state.page}`);
const movieList = response.data.results.slice(0, 10);
const movies = movieList.map(movie => movie);
const totalPages = response.data.total_pages;
this.setState({
...this.state,
allMovieList: movies,
})
} catch (event) {
console.log(event);
}
}
onNextPage = () => {
this.setState((previousState, currentProps) => {
return { page: previousState.page + 1 };
});
}
render() {
return (
<div className='MovieList'>
...
<button onClick={this.onNextPage}></button>
</div>
);
}
To solve this, I tried to call the changePage() function inside my onNextPage() function.
onNextPage = () => {
this.setState((previousState, currentProps) => {
return { page: previousState.page + 1 };
});
this.changePage();
}
This partially solved this issue but for some reason the next page is actually only rendered on second click! I also noticed my component is being re-rendered twice on every click.
I also tried to call the changePage() function inside componentDidUpdate(), this solved the issue but now my app is constantly re-rendering which causes huge performance issues.
Can anyone help me with this? It would be greatly appreciated!
Option 1) Use setState's callback function:
this.setState((previousState, currentProps) => {
return { page: previousState.page + 1 };
}, this.changePage); // state will be updated when this gets called
Option 2) Use function arguments:
async changePage(page) {
try {
const response = await
axios.get(`https://api.themoviedb.org/4/discover/movie?page=${page}`);
...
}
}
...
onNextPage = () => {
this.setState((previousState, currentProps) => {
const page = previousState.page + 1
this.changePage(page);
return { page };
});
}
Related
I am trying to access the res.data.id from a nested axios.post call and assign it to 'activeId' variable. I am calling the handleSaveAll() function on a button Click event. When the button is clicked, When I console the 'res.data.Id', its returning the value properly, but when I console the 'activeId', it's returning null, which means the 'res.data.id' cannot be assigned. Does anyone have a solution? Thanks in advance
const [activeId, setActiveId] = useState(null);
useEffect(() => {}, [activeId]);
const save1 = () => {
axios.get(api1, getDefaultHeaders())
.then(() => {
const data = {item1: item1,};
axios.post(api2, data, getDefaultHeaders()).then((res) => {
setActiveId(res.data.id);
console.log(res.data.id); // result: e.g. 10
});
});
};
const save2 = () => {
console.log(activeId); // result: null
};
const handleSaveAll = () => {
save1();
save2();
console.log(activeId); // result: again its still null
};
return (
<button type='submit' onClick={handleSaveAll}>Save</button>
);
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, like in your example, the console.log function runs before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(activeId);
}, [activeId);
The callback will run every time the state value changes and only after it has finished changing and a render has occurred.
Edit:
Based on the discussion in the comments.
const handleSaveSections = () => {
// ... Your logic with the `setState` at the end.
}
useEffect(() => {
if (activeId === null) {
return;
}
save2(); // ( or any other function / logic you need )
}, [activeId]);
return (
<button onClick={handleSaveSections}>Click me!</button>
)
As the setState is a async task, you will not see the changes directly.
If you want to see the changes after the axios call, you can use the following code :
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
setTimeout(()=>console.log(activeId),0);
})
useEffect(() => {
}, [activeId]);
const [activeId, setActiveId] = useState(null);
const save1 = () => {
const handleSaveSections = async () => {
activeMetric &&
axios.get(api1, getDefaultHeaders()).then(res => {
if (res.data.length > 0) {
Swal.fire({
text: 'Record already exists',
icon: 'error',
});
return false;
}
else {
const data = {
item1: item1,
item2: item2
}
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
})
}
});
}
handleSaveSections()
}
const save2 = () => {
console.log(activeId); //correct result would be shown here
}
const handleSaveAll = () => {
save1();
save2();
}
return (
<button type="submit" onClick={handleSaveAll}>Save</button>
)
here is the context
I am invoking a function invoked(..) from another component. Inside invoked I setState but the state does not get set. It does not trigger the useEffect nor does it take the updated value in the useInterval.
Below is the code.
Component 1 = exampleComp.ts
const exampleComp = ({initValue: ExampleProps}) {
const [params, setParams] = useState<{field1: null | string}> ({field1: null});
const invoked = (x: string) => {
console.log("inside invoked"); // this gets printed
setParams({"field1": x});
};
useEffect(() => {
console.log(params); // does not come here :(
}, [params]);
const fetchData = {...};
useInterval(
() => {
if (!params.field1 || fetching) return;
console.log("in interval", params); // does not come here too :(
fetchData(params.field1);
},
params && !fetching ? 5000: null,
);
return {invoked, ...}
}
Component 2:
const newComp = ({initValue: ExampleProps}) {
const {invoked, ..} = useExampleComp({...}); //Example comp is the above component.
useEffect(() => {
invoked(x);
}, []);
}
Any help will be appreciated! thanks.
Turns out that the component was unmounting too early. This happens when the scope of the component is too narrow.
If you have a similar problem, try logging the component to see if this is indeed happening.
I understand that a fully updated state is passed to setState's callback. I would like to know, though, if this.state is also current at that time. The reason being that I have a method which uses this.state, and I'd like to know if it's safe to call from inside the callback. Example:
areWeBigger = () => {
const { theirSize } = this.props;
const { ourSize } = this.state;
return ourSize > theirSize;
};
attemptToEat = () => {
this.setState(({ ourSize }) => {
const bigger = this.areWeBigger(); // Is this safe?
return { ourSize: bigger ? ourSize + 1 : ourSize - 1 };
});
};
EDIT
I realize now that what I've been calling the "callback" is actually called the "updater". Sorry for the confusion.
I think it should work. The syntax is also a bit different from what you have used. You are not using the callback in the above example, but rather using the functional convention of setState method. Please refer React JS Documentation for more information about the same. Also, the following snippet might present a better approach towards what you are trying to achieve.
const areWeBigger = () => {
const { theirSize } = this.props;
const { ourSize } = this.state;
return ourSize > theirSize;
};
const attemptToEat = () => {
this.setState({ ourSize: "whatever value" }, () => {
const bigger = this.areWeBigger(); // it is okay to use this
// it doesn't return anything, that is not supported so remove your return statement
// ideally you can also use componentDidUpdate lifecycle method if you don't want to always trigger a callback, you can refer react docs.
})
});
};
It seems you don't need setState callback in your case.
From here, you can call setState like this:
this.setState((prevState, props) => {
return {counter: prevState.counter + props.step};
})
So you can call your areWeBigger function like this:
areWeBigger = (ourSize , theirSize ) => {
return ourSize > theirSize;
};
attemptToEat = () => {
this.setState(({ ourSize }, {theirSize }) => { //extract ourSize and theiSize from (current)state and props respectively
const bigger = this.areWeBigger(ourSize, theirSize ); // this is before the state is updated
return { ourSize: bigger ? ourSize + 1 : ourSize - 1 };
});
};
To keep it in your way with direct state access in "areWeBigger":
areWeBigger = () => {
const {ourSize} = this.state;
const {theirSize} = this.props;
return ourSize > theirSize;
};
callback = () => {
// ... here the state is up to date
}
attemptToEat = () => {
this.setState(({ ourSize }) => { //extract ourSize and theiSize from (current)state and props respectively
const bigger = this.areWeBigger(); // this is before the state is updated
return { ourSize: bigger ? ourSize + 1 : ourSize - 1 };
}, callback); // to see the use of callback
};
I'm quite new to React and I don't always understand when I have to use hooks and when I don't need them.
What I understand is that you can get/set a state by using
const [myState, setMyState] = React.useState(myStateValue);
So. My component runs some functions based on the url prop :
const playlist = new PlaylistObj();
React.useEffect(() => {
playlist.loadUrl(props.url).then(function(){
console.log("LOADED!");
})
}, [props.url]);
Inside my PlaylistObj class, I have an async function loadUrl(url) that
sets the apiLoading property of the playlist to true
gets content
sets the apiLoading property of the playlist to false
Now, I want to use that value in my React component, so I can set its classes (i'm using classnames) :
<div
className={classNames({
'api-loading': playlist.apiLoading
})}
>
But it doesn't work; the class is not updated, even if i DO get the "LOADED!" message in the console.
It seems that the playlist object is not "watched" by React. Maybe I should use react state here, but how ?
I tested
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
//refresh playlist if its URL is updated
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
})
}, [props.playlistUrl]);
And this, but it seems more and more unlogical to me, and, well, does not work.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setPlaylist(playlist); //added this
})
}, [props.playlistUrl]);
I just want my component be up-to-date with the playlist object. How should I handle this ?
I feel like I'm missing something.
Thanks a lot!
I think you are close, but basically this issue is you are not actually updating a state reference to trigger another rerender with the correct loading value.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
setPlaylist(playlist); // <-- this playlist reference doesn't change
})
}, [props.playlistUrl]);
I think you should introduce a second isLoading state to your component. When the effect is triggered whtn the URL updates, start by setting loading true, and when the Promise resolves update it back to false.
const [playlist] = React.useState(new PlaylistObj());
const [isloading, setIsLoading] = React.useState(false);
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setIsLoading(false);
});
}, [props.playlistUrl]);
Use the isLoading state in the render
<div
className={classNames({
'api-loading': isLoading,
})}
>
I also suggest using the finally block of a Promise chain to end the loading in the case that the Promise is rejected your UI doesn't get stuck in the loading "state".
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl)
.then(function() {
console.log("LOADED!");
})
.finally(() => setIsLoading(false));
}, [props.playlistUrl]);
Here you go:
import React from "react";
class PlaylistAPI {
constructor(data = []) {
this.data = data;
this.listeners = [];
}
addListener(fn) {
this.listeners.push(fn);
}
removeEventListener(fn) {
this.listeners = this.listeners.filter(prevFn => prevFn !== fn)
}
setPlayList(data) {
this.data = data;
this.notif();
}
loadUrl(url) {
console.log("called loadUrl", url, this.data)
}
notif() {
this.listeners.forEach(fn => fn());
}
}
export default function App() {
const API = React.useMemo(() => new PlaylistAPI(), []);
React.useEffect(() => {
API.addListener(loadPlaylist);
/**
* Update your playlist and when user job has done, listerners will be called
*/
setTimeout(() => {
API.setPlayList([1,2,3])
}, 3000)
return () => {
API.removeEventListener(loadPlaylist);
}
}, [API])
function loadPlaylist() {
API.loadUrl("my url");
}
return (
<div className="App">
<h1>Watching an object by React Hooks</h1>
</div>
);
}
Demo in Codesandbox
Suppose there is a component where ask server to do some search and response will be rendered. How to ensure most recent request's response is rendered even if server side for any reason answers in different ordering? I'm not asking about cancelling previous request since it's not always possible with reasonable efforts.
onClick = () => {
apiCall(this.state.searchQuery).then(items => this.setState({ items }));
};
Is there elegant way to handle that? By now I know few approaches:
disabling button till request comes(provides bad experiences in large amount of cases - say for searching while typing)
checking inside then() if request's params matches this.props/this.state data(does not handle case when we intentionally forced new search with same query - say by pressing Enter/clicking "Search" button)
onClick = () => {
const searchQuery = this.state.searchQuery;
apiCall(searchQuery)
.then(items =>
this.state.searchQuery === searchQuery
&& this.setState({ items })
);
};
marking requests somehow and checking if it's latest(works, but looks too verboose especially if there are few requests we need to check)
searchQueryIndex = 0;
onClick = () => {
this.searchQueryIndex++;
const index = this.searchQueryIndex;
apiCall(this.state.searchQuery)
.then(items =>
this.searchQueryIndex === searchQueryIndex
&& this.setState({ items })
);
};
I'd call that trio "ugly, broken and messy".
Is there something such clear way as hooks allow:
useEffect(() => {
const isCanceled = false;
apiCall(searchQuery).then(items => !isCanceled && setItems(items));
return () => {isCanceled = true;};
}, [searchQuery])
Your onClick handler suggest a class component since you use this and this.setState:
onClick = () => {
apiCall(this.state.searchQuery).then(items =>
this.setState({ items })
);
};
I adjusted onlyLastRequestedPromise to take a function that will return something (you can return Promise.reject('cancelled') or anything).
const onlyLastRequestedPromise = (promiseIds => {
const whenResolve = (
promise,
id,
promiseID,
resolveValue,
whenCancelled = () => Promise.reject('cancelled')
) => {
if (promise !== undefined) {
//called by user adding a promise
promiseIds[id] = {};
} else {
//called because promise is resolved
return promiseID === promiseIds[id]
? Promise.resolve(resolveValue)
: whenCancelled(resolveValue);
}
return (function(currentPromiseID) {
return promise.then(function(result) {
return whenResolve(
undefined,
id,
currentPromiseID,
result
);
});
})(promiseIds[id]);
};
return (id = 'general', whenCancelled) => promise =>
whenResolve(
promise,
id,
undefined,
undefined,
whenCancelled
);
})({});
A class example on how to use it:
class Component extends React.Component {
CANCELLED = {};
last = onlyLastRequestedPromise(
'search',
() => this.CANCELLED
);
onSearch = () => {
this.last(apiCall(this.state.searchQuery)).then(
items =>
items !== this.CANCELLED && this.setState({ items })
);
};
changeAndSearch = e => {
this.setState(
{}, //state with new value
() => this.onSearch() //onSearch after state update
);
};
render() {
return (
<div>
<SearchButton onClick={this.onSearch} />
<Other onChange={this.changeAndSearch} />
</div>
);
}
}
I agree it's a lot of code but since you put most of the implementation in the lib it should not clutter your components.
If you had a functional component you could create the last function with useRef:
//
function ComponentContainer(props) {
const CANCELLED = useRef({});
const last = useRef(
onlyLastRequestedPromise('search', () => CANCELLED)
);
const [searchQuery,setSearchQuery] = useState({});
const mounted = useIsMounted();
const onSearch = useCallback(
last(apiCall(searchQuery)).then(
items =>
items !== CANCELLED &&
mounted.current &&
//do something with items
)
);
}
Finally figured out how to utilize closure to mimic "just ignore that" approach from hooks' world:
class MyComponent extends React.Component {
const ignorePrevRequest = () => {}; // empty function by default
loadSomeData() {
this.ignorePrevRequest();
let cancelled = false;
this.ignorePrevRequest = () => { cancelled = true; }; // closure comes to play
doSomeCall().then(data => !cancelled && this.setState({ data }))
}
}