Rory
Rory

Reputation: 1492

React / Redux Async not waiting for returned response

First of all my code is working (everything is exported correctly etc ) but it's not waiting for the async return of data. I'm using redux-thunk for my async middleware

I have an action named async.js

export function itemsHasErrored(bool) {
    return {
        type: 'ITEMS_HAS_ERRORED',
        hasErrored: bool
    };
}

export function itemsIsLoading(bool) {
    return {
        type: 'ITEMS_IS_LOADING',
        isLoading: bool
    };
}

export function itemsFetchDataSuccess(items) {
    return {
        type: 'ITEMS_FETCH_DATA_SUCCESS',
        items
    };
}

export function itemsFetchData(url) {
    return (dispatch) => {
        dispatch(itemsIsLoading(true));

        fetch(url)
            .then((response) => {
                if (!response.ok) {
                    throw Error(response.statusText);
                }

                dispatch(itemsIsLoading(false));

                return response;
            })
            .then((response) => response.json())
            .then((items) => dispatch(itemsFetchDataSuccess(items)))
            .catch(() => dispatch(itemsHasErrored(true)));
    };
}

My reducer

export function itemsHasErrored(state = false, action) {
    switch (action.type) {
        case 'ITEMS_HAS_ERRORED':
            return action.hasErrored;

        default:
            return state;
    }
}

export function itemsIsLoading(state = false, action) {
    switch (action.type) {
        case 'ITEMS_IS_LOADING':
            return action.isLoading;

        default:
            return state;
    }
}

export function items(state = [], action) {
    switch (action.type) {
        case 'ITEMS_FETCH_DATA_SUCCESS':
            return action.items;

        default:
            return state;
    }
}

I have a container component, asyncContainer.js

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'

import {itemsFetchData} from '../../../actions/async';
import AsyncUI from './asyncUI'

class AsyncContainer extends Component {

    componentDidMount() {
        this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
    }

    render() {

        if (this.props.hasErrored) {
            return <p>Sorry! There was an error loading the items</p>;
        }

        if (this.props.isLoading) {
            return <p>Loading…</p>;
        }
//This fails to wait
        return (
               <AsyncUI />
        );
    }
}

AsyncContainer.propTypes = {
    fetchData: PropTypes.func.isRequired,
    items: PropTypes.array.isRequired,
    hasErrored: PropTypes.bool.isRequired,
    isLoading: PropTypes.bool.isRequired
};

const mapStateToProps = (state) => {
    return {
        items: state.items,
        hasErrored: state.itemsHasErrored,
        isLoading: state.itemsIsLoading
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        fetchData: (url) => dispatch(itemsFetchData(url))
    };
};

export default connect(mapStateToProps, mapDispatchToProps)(AsyncContainer);

And finally I have a simple UI component named asyncUI.js written in a functional way

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'

const AsyncUI = (items) => {

    return (
        <ul>
            {items.map((item) => (
                <li key={item.id}>
                    {item.label}
                </li>
            ))}
        </ul>
    );
}

const mapStateToProps = (state) => {
    return {
        items: state.items
    };
};

export default connect(mapStateToProps)(AsyncUI);

In asyncContainer.js you can see the call to my simple UI component

 return (
               <AsyncUI />
        );

But on calling the property of the redux store items in asyncUI.js an empty array, therefore the items.map fails

However, if I remove the code from asyncUI.js and place it in asyncContainer.js it works

This is the code that works in asyncContainer.js

  class AsyncContainer extends Component {
        componentDidMount() {
            this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
        }

        render() {
            if (this.props.hasErrored) {
                return <p>Sorry! There was an error loading the items</p>;
            }

            if (this.props.isLoading) {
                return <p>Loading…</p>;
            }

//THIS IS WHERE I HAD <Async />
            return (
                <ul>
                    {this.props.items.map((item) => (
                        <li key={item.id}>
                            {item.label}
                        </li>
                    ))}
                </ul>
            );
        }
    }

    AsyncContainer.propTypes = {
        fetchData: PropTypes.func.isRequired,
        items: PropTypes.array.isRequired,
        hasErrored: PropTypes.bool.isRequired,
        isLoading: PropTypes.bool.isRequired
    };

    const mapStateToProps = (state) => {
        return {
            items: state.items,
            hasErrored: state.itemsHasErrored,
            isLoading: state.itemsIsLoading
        };
    };

    const mapDispatchToProps = (dispatch) => {
        return {
            fetchData: (url) => dispatch(itemsFetchData(url))
        };
    };

    export default connect(mapStateToProps, mapDispatchToProps)(AsyncContainer);

I think the problem is that the component is rendering before the items data is ready. This is normal react behavior. So how do I "hold off" the rendering. As you can see I'm trying to use a Container /Component style of architecture. I can always use the solution that works as mentioned above, but I'd like to stick with this Container /Component. Am I going to have to delve deeper into Promises etc ? Should I not use the functional way of writing for asyncUI.js ? I'm a little confused.

Upvotes: 0

Views: 1049

Answers (3)

Rory
Rory

Reputation: 1492

see Michael Peyper for a good answer

It turns out that the problem was with the functional style of coding of my asyncUI component. I converted it back to the 'standard' stateful component and bingo it worked.

asyncContainer.js

class AsyncContainer extends Component {

    componentDidMount() {
        this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
    }

    render() {

          if (this.props.hasErrored) {
                  return <p>Sorry! There was an error loading the items</p>;
              }

              if (this.props.isLoading) {
                  return <p>Loading…</p>;
              }
              return (
                  <AsyncUI />     
        )

    }
}

asyncUI.js

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'

    class AsyncUI extends Component {

        render() {
                return (
                <ul>
                {this.props.items.map((item) => (
                    <li key={item.id}>
                        {item.label}
                    </li>
                ))}
            </ul>   
            )
        }
    }

    const mapStateToProps = (state) => {
        return {
            items: state.items,
        };
    };

export default connect(mapStateToProps)(AsyncUI);

Hope this helps anyone :-)

Upvotes: 0

Michael Peyper
Michael Peyper

Reputation: 6944

Try:

const AsyncUI = ({items}) => {
              /* ^ see ^ */
    return (
        <ul>
            {items.map((item) => (
                <li key={item.id}>
                    {item.label}
                </li>
            ))}
        </ul>
    ); }

This pulls the items value off the props you reacted in the mapStateToProps function, which is an object, not an array (hence no map function).

NOTE: This should fix your issue, but it is still technically trying to render the items before they are ready in 2 instances:

  1. The first time the component renders. The initial state for itemsIsLoading is false, so the first render will fail all the safety checks. The default value for items is [] so it should just render <ul></ul> for a very brief moment until the itemsIsLoading(true) action is dispatched. You can set the initial state to true for stop this, or change the loading check to be

    if (this.props.isLoading || this.props.items.length != 0) {
        return <p>Loading…</p>;
    }
    

    An argument can be made for how necessary either of those solutions actually are.

  2. After the fetch returns the order the actions is dispatched in will result in another brief render of <ul></ul> as the loading state is set to false before the items are set. See dgrijuela's answer for one way to fix this. Another way would be to not dispatch seperate actions and have the ITEMS_FETCH_DATA_SUCCESS and ITEMS_HAS_ERRORED actions also change the itemsIsLoading value back to false (multiple reducers can act on the same action type).

Upvotes: 2

dgrijuela
dgrijuela

Reputation: 763

You call dispatch(itemsIsLoading(false)) before dispatch(itemsFetchDataSuccess(items))

Try like this:

// async.js
...

export function itemsFetchData(url) {
  return (dispatch) => {
    dispatch(itemsIsLoading(true));

    fetch(url)
        .then((response) => {
            if (!response.ok) {
                throw Error(response.statusText);
            }

            return response;
        })
        .then((response) => response.json())
        .then((items) => {
          dispatch(itemsFetchDataSuccess(items));
          dispatch(itemsIsLoading(false));
        })
        .catch(() => dispatch(itemsHasErrored(true)));
    };
}

Upvotes: 0

Related Questions