React/Redux - Why this Promise.all continues to execute after dispatch? - javascript

I have a React-Native project with redux.
I perform some axios calls, and basically, the app works fine.
But, there is one thing that is not right.
I put a console.log before the dispatch, and even the app loads and renders everything just fine, I see the console.log looping on and on in the console.
I'm not sure why this is happening, but I read the "run to completion" concept in Javascript, which can be the reason. Even though, I couldn't figure it out.
any ideas how can I fix this? thank you very much.
UPDATE: here is the component that invokes the action, in the renderEtiquetas() function. This can be the reason causing this loop, since it runs on every re-render cycle (not sure about this). I tried moving the invoke to componentDidMount() but it didn't seem to run.
I'm new to React so I'm probably doing something dumb.
component.js
class EtiquetasList extends Component {
componentDidMount() {
this.props.FetchEtiquetas();
}
renderEtiquetas() {
if ( this.props.etiquetas.length == 0 ) {
return <ActivityIndicator size="large" color="#00ff00" />
} else {
this.props.FetchGalleries( this.props.etiquetas );
if ( this.props.galleries.length > 0 ) {
return this.props.etiquetas.map(etiqueta =>
<EtiquetaDetail key={etiqueta.id} etiqueta={etiqueta} galleries={ this.props.galleries } />
);
}
}
}
render() {
return (
<ScrollView>
{ this.renderEtiquetas() }
</ScrollView>
);
}
}
const mapStateToProps = (state) => {
return {
etiquetas: state.data.etiquetas,
isMounted: state.data.isMounted,
galleries: state.slides.entries
};
};
export default connect(mapStateToProps, { FetchEtiquetas, FetchGalleries })(EtiquetasList);
actions.js
export function FetchGalleries( etiquetas ) {
return function (dispatch) {
return Promise.all(
etiquetas.map( record =>
axios.get('mydomain.com/?id='+record.id)
)).then(galleries => {
let my_data = [];
let data_json = '';
galleries.map( record => {
record.data.map( subrecord => {
// this is simplified for this example, it works as intended
data_json = data_json + '{ title: "' + subrecord.title+'"}';
});
my_data.push( data_json );
});
console.log( my_data ); // this keeps printing in the console
return dispatch({ type: FETCH_GALLERIES_SUCCESS, payload: my_data });
});
}
}

Aha, FetchGalleries is running inside the render function, it will cause an action->render->action->render infinite loop.
Edit:
How about trying to merge your FetchGalleries and FetchEtiquetas into one action:
export const fetchGalleries = () => {
return function (dispatch) {
return axios.get('/path/to/etiquetas').then(etiquetas => {
dispatch({ type: FETCH_ETIQUETAS_SUCCESS, payload: etiquetas });
// ... your previous promise all code
});
}
}
and only need to call this new fetchGalleries at componentDidMount.

You are close, you just need to return or await Promise.all otherwise it will not be awaited
export function FetchGalleries( etiquetas ) {
return function (dispatch) {
return Promise.all(....
}
// UPDATE: The answer from 李骏骁 is correct

Related

Redux state is not updating after adding a blog post

Okay so I have this React app with a backend built with Node and Express. I got two endpoints, getAllPosts and createPost. getAllPosts returns an array of posts and createPosts returns an object with status and message.
So in the frontend I am fetching the data in Redux actions like so:
export const getPosts = () => async ( dispatch ) => {
try {
const config = {
headers: {
'Content-Type': 'application/json'
}
}
const { data } = await axios.get( '/api/posts', config );
dispatch({
type: GET_POSTS_SUCCESS,
payload: data
});
} catch ( error ) {
console.log( error )
}
}
export const createPost = ( postData ) => async ( dispatch ) => {
try {
const config = {
headers: {
'Content-Type': 'application/json'
}
}
const { data } = await axios.post( '/api/post', postData, config );
dispatch({
type: CREATE_POST_SUCCESS,
payload: data
});
} catch ( error ) {
console.log( error )
}
}
And the reducers look like this:
export const getPostsReducer = ( state = [], action ) => {
switch ( action.type ) {
case GET_POSTS_SUCCESS:
return {
loading: false,
posts: action.payload
};
default:
return state;
}
}
export const createPostReducer = ( state = [], action ) => {
switch ( action.type ) {
case CREATE_POST_SUCCESS:
return {
loading: false,
response: action.payload
};
default:
return state;
}
}
I can successfully render all posts in the first page load but when I create a post, the newly created one is not added right away. If I check the Redux dev tools, I don't see the updated list. But only renders or dev tools show the updated list after I reload the page. .
I am dispatching getPosts in one component like so:
useEffect( () => {
dispatch( getPosts() );
}, [ dispatch ] )
and dispatching the createPost in another component's submitHandler like this:
const submitHandler = ( e ) => {
e.preventDefault();
dispatch( createPost( { post } ) );
}
I also tried putting the submitHandler in the same component where getPosts is called so that dispatching it would trigger the useEffect and call getPosts again to get the updated list. But that doesn't work either. Besides that I also tried adding the returned list from getPosts as dependency in useEffect but if I log it then there are infinite amount gets printed on the log.
What am I doing wrong here?
The problem seems to be that React doesn't know that a new post was created. You'll need to call getPosts() after createPost() has been called.
const submitHandler = (e) => {
e.preventDefault();
dispatch(createPost({ post }));
dispatch(getPosts());
}
If you want to structure your services in a more centralized, scalable way, check out my blog post on React services: https://schneider-lukas.com/blog/react-connect-rest-api. You could just as well store the posts in the Provider component to make them available to all components in the tree, or better yet create a separate Context, Provider, and Consumer for handling posts.
I'd change the structure a bit:
in the createPost method instead of dispatching I'd just return data
export const createPost = ( postData ) => async ( dispatch ) => {
try {
const config = {
headers: {
'Content-Type': 'application/json'
}
}
const { data } = await axios.post( '/api/post', postData, config );
return data;
} catch ( error ) {
console.log( error )
}
}
and then in your code I'd
const submitHandler = ( e ) => {
e.preventDefault();
createPost({post}).then(data=>{
dispatch({type:CREATE_POST, data})
}
this should definitely work

NextJS getServerSideProps pass data to Page Class

i know this has probably been already asked, but i'm at a point where i don't know what to do.
I'm not a (very) experienced developer in javascript or NextJS.
My Problem(1):
I got the method: export const getServerSideProps: GetServerSideProps = async () => {} implemented to fetch some data from a integrated API (pages/api from NextJS). The code itself is probably not well(or worse) written, but it works. (for now at least)
export const getServerSideProps: GetServerSideProps = async () => {
try {
// get userID
await fetch("http://localhost:32147/api/v1/user/get?requestedField=userID&fieldName=username&fieldValue=<value removed>").then(
(userIDResponse: Response): any => {
// get userID as json
userIDResponse.json().then((userIDResult: Response): any => {
// get messages
fetch(
"http://localhost:32147/api/v1/message/get?requestedField=*&fieldName=userID&fieldValue=" +
JSON.stringify(userIDResult[0].userID)
).then((messageResponse: Response): any => {
// get messages as json
messageResponse.json().then((messageResult) => {
return {
props: { messages: messageResult },
{/* marker1 */}
}
})
})
})
}
)
} catch (error) {
console.log(error)
}
}
just to be clear, this method works, data fetching works but just if i access it at marker1
that one part where i return the props:
return {
props: { messages: messageResult },
}
i can't do that 'cause nextjs is gonna break because of getServerSideProps() didn't return anything.
I tried to store the final data into a variable, that i declared on the first line of this method, but it ended up being empty the whole time.
How can i solve this?
My Problem(2): if i set a manual value at the end of this method for testing, it doesn't get passed to the main Page Class (index.tsx)
i can just access it using this.props.<prop name>, in this case: this.props.messages, right?
The whole index.tsx:
import React, { Component } from "react"
import { GetServerSideProps } from "next"
import Router from "next/router"
import Head from "next/head"
import Navbar from "../lib/Navbar"
import MessagesModal from "../lib/MessagesModal"
export const getServerSideProps: GetServerSideProps = async () => {
try {
// get userID
await fetch("http://localhost:32147/api/v1/user/get?requestedField=userID&fieldName=username&fieldValue=<value removed>").then(
(userIDResponse: Response): any => {
// get userID as json
userIDResponse.json().then((userIDResult: Response): any => {
// get messages
fetch(
"http://localhost:32147/api/v1/message/get?requestedField=*&fieldName=userID&fieldValue=" +
JSON.stringify(userIDResult[0].userID)
).then((messageResponse: Response): any => {
// get messages as json
messageResponse.json().then((messageResult) => {
return {
props: { messages: messageResult },
}
})
})
})
}
)
} catch (error) {
console.log(error)
}
}
interface HomeProps {
messages?: []
}
export default class Home extends Component<HomeProps> {
constructor(props) {
super(props)
}
state = {
messagesModal: false,
messages: [],
}
// triggers logout
triggerLogOut(): void {}
render(): JSX.Element {
return (
<>
<Head>
<title>OneDrive Event Connector</title>
</Head>
<Navbar
ItemClickCallback={(callbackItem: string): void => {
if (callbackItem === "messages") {
this.setState({ messageModal: !this.state.messageModal })
} else if (callbackItem === "log_out") {
this.triggerLogOut()
} else {
Router.push("/" + callbackItem)
}
}}
/>
<div className="app-content"></div>
<MessagesModal
messages={this.props.messages}
isOpen={this.state.messagesModal}
toggleModal={() => {
this.setState({ messageModal: !this.state.messagesModal })
}}
/>
</>
)
}
}
This is just a "fun" project for me to practise and learn.
Would be greate if anyone could give me even a hint on what is my problem/mistake here...
Thanks.
Kind regards
Oliver
i can't do that 'cause nextjs is gonna break because of getServerSideProps() didn't return anything.
exactly - in your code, you are returning values inside of a chain of promises - you need to make sure, that values are returned from each step
here's a working example - similar flow with swapped API - to help you understand how to return something, going back from the inside of your chained promises
export const getServerSideProps: GetServerSideProps = async () => {
try {
// initial fetch
const result = await fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((todosResponse: Response): any => {
return todosResponse.json().then((todo) => {
// fetch something more
return fetch(
"https://jsonplaceholder.typicode.com/users/" + todo.userId
).then((userResponse: Response): any => userResponse.json());
})
})
return {
props: { messages: result },
}
} catch (error) {
console.log(error)
}
}
My advise is also to read more on promises / async await in JS world
My Problem(2)
i can just access it using this.props., in this case: this.props.messages, right?
yes, that's right
interface HomeProps {
messages?: []
}
export default class Home extends Component<HomeProps> {
constructor(props) {
super(props)
}
render() {
return (
<>
{JSON.stringify(this.props.messages)}
</>
)
}
}

Setting state in getDetails() occurs infinite loop

this.state = {
data: [],
details: []
}
componentDidMount() {
this.getDetails()
this.getCountries()
}
getCountries() {
Utils.rest('POST', 'https:///api-spot-get-all', {
country: '',
windProbability: ''
}).then(async (r) => {
const data = await r.json();
this.setState({
data: data.result
})
}).catch(err => {
console.log(err.message);
});
}
`getDetails() {
return new Promise((resolve, reject) => {
let details_list = [];
this.state.data.map(item => {
return (
Utils.rest('POST', 'https:///api-spot-get-details', {
spotId: item.id
})
.then(async (r) => {
const details_item = await r.json()
console.log(`Loaded ${details_list.length} item ...(of ${this.state.data.length})`);
if (details_list.length === this.state.data.length) {
await resolve(details_list)
}
details_list.push(details_item.result);
})
);
})
})
}`
render() {
return (
{
this.state.data.map((item, key) => {
return (
{item.id}
{item.id}
);
})
}
Here is my code. After first call I am receiving id and passing it as input to second call
I think this is happening because you're calling this.getCountries() in the render function. So the function is called in every render, that causes a new request that sets a new state, which will trigger a new render an so on, creating an infinite loop. So, if you delete the function calling from the render function it should work.
This is a basic example:
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const myarray=[1,2,3,4,5]
const mytable=myarray.map((item,key)=>{return(
<table key={key}>
<tr><td>id</td><td>{item}</td></tr>
</table>)
})
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{mytable}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You can create a const in the render and use after in return so you can tryin your code to do something like that:
render() {
const myComponent= this.state.data.map((item, key) => { return (
<div key={key}>
<span>{item.it}</span>
</div>
) });
return (
{myComponent}
)
}
I used and is just for example you can use what structor you want as in the first example I used table...
Please note that you are calling getDetails method in render. render is not a good place to add methods which modify the internal state. please check the react doc for additional details.
There are a lot of strange things there. First of all, getDetails returns a Promise, but the promise is not resolved anywhere. Its usage should be something like:
getDetails()
.then(data => {
// do something with the data
}, error => {
// manage here the error
}):
Also, this.state.data.map should be this.state.data.forEach and delete the return from inside, because you don't need to return anything outside
On the other hand, there's an issue with getCountries. Its name sais it's a GET, but the API call sends a POST.
After that's clarified, inside getDetails you're using the data retrieved in getCountries, so its call should be inside the request resolving inside getCountries or either change getCountries to a Promise and do something like:
this.getCountries()
.then(data => {
this.getDetails();
});
You don't care when the getDetails call ends, so it doesn't need to return a Promise.
And, in the end, the render function should look more like this:
render() {
return (
<div>
{this.state.data.map((item, key) =>
<div key={key}>{item.id} - {item.id}</div>
)}
</div>
)
}
After this it should work properly, more or less. I have to warn you, though. Probably you would need to do something easier to become familiar with React's flow and how to properly work with state and JS's asynchronous functions.

Await for Redux thunk in react lifecycle

I'd been working with React & Redux for some time when a work
colleague saw some code I wrote and commented on it.
SomeComponent.js
class SomeComponent extends Component {
async componentDidMount() {
await this.props.fetchPosts();
if (this.props.posts.length < 1)
return navigateTo( /* someOtherPlace */);
}
render() {
return (
<>
{this.props.posts.map(
(postData, i) => <Post key={i} {...postData}/>
)}
</>
);
}
}
const mapStateToProps = ({ posts }) => ({
posts: posts.list,
isFetching: posts.isFetching
});
export default connect(mapStateToProps, { fetchPosts })(SomeComponent);
actions/posts.js
export const fetchPosts = () => async dispatch => {
dispatch(requestPosts());
let posts;
try {
posts = (await api.get('/posts')).data
} catch (e) {
posts = e;
}
dispatch(receivePosts(posts));
}
He basically said that I shouldn't be awaiting for fetchPosts() action, instead I should just call it, and let it update props, re-render and perform the conditional navigation in componentDidUpdate which when he said it, it totally made sense to me.
But now I keep asking myself if what I was doing was really that bad, potencially buggy or just a bad practice that added more complexity.
He didn't mention the reasons why it was wrong other than it wasn't the React way of doing it.
EDIT: Added code snippet showing that the approach actually does work and doesn't perform faulty reads.
So there is small issue in your case
async componentDidMount() {
await this.props.fetchPosts();
if (this.props.posts.length < 1)
return navigateTo( /* someOtherPlace */);
}
Here await will wait till the fetchPosts is completed provided it returns a promise. Now fetchPosts will dispatch an action that will only result in the props being updated and another render triggered to update the posts in the component. So even if you wait for fetchPosts to complete the posts are not updated in the same render cycle and hence using this.props.posts.length won't return you the result corresponding to the latest posts update in redux store. The result being that you are unnecessarily waiting for the fetchPosts to complete and performing a check which will lead to a faulty result
Better approach is to work like
class SomeComponent extends Component {
componentDidMount() {
this.props.fetchPosts();
}
componentDidUpdate(prevProps) {
if(this.props.posts !== prevProps.posts && this.props.posts.length < 1) {
return navigateTo( /* someOtherPlace */);
}
}
render() {
return (
<>
{this.props.posts.map(
(postData, i) => <Post key={i} {...postData}/>
)}
</>
);
}
}

My React/Redux API wont log out properly shows [[PromiseValue]]

Quite new to coding but hitting hurdles with a React/Redux API.
I'm following Bucky Roberts' YouTube tutorial & ive replaced his static names object with an API that i then pull into my reducer.
This logs out the data fine, as an array - but when i try to pull it in to my Container, i get either a map is not a function error (even though it is an array) or if i log out console.log('props: ', this.props) i get [[PromiseValue]] with the array[10] showing.
But i'm pretty sure drilling into that in my component would be a no go.
CONTAINER:
class UserList extends Component {
createListItems() {
console.log('props: ', this.props);
return this.props.users.map((user) => {
return (
<li
key={user.id}
onClick={() => this.props.selectUser(user)}
>
{user.name}
</li>
)
})
}
render() {
return (
<div>
<ul>
{this.createListItems()}
</ul>
</div>
)
}
}
function mapStateToProps(state) {
return {
users: state.users
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({selectUser: selectUser}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(UserList);
API:
const MyData = fetch('https://jsonplaceholder.typicode.com/users')
.then(function(data1) {
return data1.json()
}).then(function(data2) {
return data2
}).then(function(data3) {
console.log('API RESPONSE: ', data3)
return data3
})
export default MyData;
MyData is then pulled into a reducer, then into combineReducers({}) and the data should show back in mapStateToProps as above in the container. What Am I Doing Wrong? I feel like it's something to do with resolving Promises but my api actually logs everything out correctly. So is it something to do with having API data passed in higher up, in createStore or?:
import MyData from './MyData'
export default function() {
return MyData
}
The proper way to deal with API requests is to put them inside actions and use some middleware to handle asynchronous functions. I'm not sure what is going on with your API function, but anyway consider this solution:
In componentWillMount call an action which fetches users, example with redux-thunk middleware:
export function fetchUsers() {
return async (dispatch) => {
const { data } = await axios.get('https://jsonplaceholder.typicode.com/users');
dispatch({
type: FETCH_USERS,
payload: data
});
}
}
Then you won't have a promise in reducer, but json object and you will be able to map the users, but remember that there is a split second when you don't have users to map, so you have to handle this condition to avoid error:
createListItems() {
if(this.props.users.length > 0) {
return this.props.users.map((user) => {
return (
<li
key={user.id}
onClick={() => this.props.selectUser(user)}
>
{user.name}
</li>
)
})
} else {
return <div>Loading...</div>;
}
}

Categories

Resources