1

So I have a component that shows categories from firestore, the component shows nothing the first time but when I click navbar button again it does show the data stored in firestore.

Here is the component file :

import * as React from "react";
import Category from "./Category";
import connect from "react-redux/es/connect/connect";
import {getCategories} from "../reducers/actions/categoryAction";

class CategoriesList extends React.Component{
    constructor(props) {
        super(props);
        this.state = ({
            categoriesList: [{}]
        })
    }

    componentWillMount() {
        this.props.getCategories();
        this.setState({categoriesList: this.props.categories});
        this.forceUpdate();
    }

    render() {
        return (
            <div className={'container categories'}>
                <div className={'row center'} onClick={() => this.props.history.push('/addcategories')}>
                    <div className={'col s24 m12'}>
                        <p>Create New Category</p>
                    </div>
                </div>

                <div className={'row'}>
                    <div className={'col s24 m12'}>
                        {/*{() => this.renderCategories()}*/}


                        {this.state.categoriesList && this.state.categoriesList.map(category => {
                            return <Category category={category} key={category.id}/>
                        })}
                    </div>
                </div>
            </div>
        );
    }
}

const mapDisptachToProps = (dispatch) => {
    return {
        getCategories: () => dispatch(getCategories()),
    }
};

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

export default connect(mapStateToProps, mapDisptachToProps)(CategoriesList)

And here is the reducer file:

 import db from '../firebaseConfig'


const initState = {
    categories: []
};

const categoryReducer = (state=initState, action) => {
    switch (action.type) {
        case 'CREATE_CATEGORY':
            db.collection("Categories").add({
                category: action.category.name
            })
                .then(function(docRef) {
                    db.collection("Categories").get().then((querySnapshot) => {
                        querySnapshot.forEach((doc) => {
                            // console.log(`${doc.id} => ${doc.data().category}`);
                            if(doc.id === docRef.id) {
                                state.categories.push({id: doc.id, name: doc.data().category});
                                console.log(state.categories)
                            }
                        });
                    });
                })
                .catch(function(error) {
                    console.error("Error adding document: ", error);
                });
            break;

        case 'GET_CATEGORIES':
            console.log('Getting data from firestore');

            db.collection("Categories").get().then((querySnapshot) => {
                if(state.categories.length !== querySnapshot.size) {
                    querySnapshot.forEach((doc) => {
                        state.categories.push({id: doc.id, name: doc.data().category});
                    });
                }
            });
            break;
    }
  return state;
};

export default categoryReducer

Is there any way to update the component after fully loading the data? or a way to load all the data in the initalState?

Kadiem Alqazzaz
  • 554
  • 1
  • 7
  • 22
  • Did you try to use ComponentDidMount instead of componentWillMount ? – Leonardo Lobato Dec 26 '18 at 17:03
  • @LeonardoLobato Yes, I have tried `componentDidUpdate(prevProps) { if (prevProps.categoriesList.length !== this.categoriesList.length) { this.forceUpdate(); } }` – Kadiem Alqazzaz Dec 26 '18 at 17:05
  • `getCategories()` is a AJAX request. When you `setState` after that request, the `categories` isn't set yet from the AJAX response. – Hoyen Dec 26 '18 at 17:06
  • @Hoyen so should I use delay? – Kadiem Alqazzaz Dec 26 '18 at 17:07
  • componentDidMount not componentDidUpdate... and probably what you probably mean was componetWillReceiveProps no componentDidUpdate. you can get ride of this.forceUpdate() if you use the right lifecycle method you probably don't need this. try the componentDidMount and if still not work try the componentWillReceiveProps – Leonardo Lobato Dec 26 '18 at 17:09
  • @SimpleWebDesigner why aren't you using a Redux action to implement the AJAX calls? https://stackoverflow.com/questions/45513318/perform-ajax-fetch-in-a-redux-reducer – Hoyen Dec 26 '18 at 17:29
  • @Hoyen I don't see a difference between my code and the post... – Kadiem Alqazzaz Dec 26 '18 at 17:37

2 Answers2

3

There are few things one needs to understand. First, this.props.getCategories() performs an action that is asynchronous in nature and hence in the very next line this.setState({categoriesList: this.props.categories});, we wont get the required data.

Second, Storing props to state without any modification is un-necessary and leads to complications. So try to use the props directly without storing it. In case you are modifying the obtained props, make sure you override getDerivedStateFromProps apropiately.

Third, Try to use componentDidMount to perform such async operations than componentWillMount. Refer when to use componentWillMount instead of componentDidMount.

Fourth(important in your case), Reducer should not contain async operations. Reducer should be a synchronous operation which will return a new state. In your case, you need to fetch the data elsewhere and then dispatch within your db.collection(..).then callback. You can also use redux-thunk, if you are using too many async operations to get your redux updated.

So @Mis94 answer should work if you follow the fourth point of returning the new state in the reducer rather than mutating the redux directly in the db().then callback

Panther
  • 8,938
  • 3
  • 23
  • 34
  • Read about Redux reducers here: https://redux.js.org/basics/reducers https://redux.js.org/recipes/structuring-reducers/structuring-reducers As mentioned in the first link: just remember that the reducer must be pure. Given the same arguments, it should calculate the next state and return it. No surprises. No side effects. No API calls. No mutations. Just a calculation. So you shouldn't be doing API calls or retrieving data inside it according to Redux documentation. – Mohamed Ibrahim Elsayed Dec 26 '18 at 22:15
0

First, you don't need to store the component's props in the state object. This is actually considered an anti-pattern in react. Instead of doing this, just use your props directly in your render method:

render() {
    return (
        <div className={'container categories'}>
            <div className={'row center'} onClick={() => this.props.history.push('/addcategories')}>
                <div className={'col s24 m12'}>
                    <p>Create New Category</p>
                </div>
            </div>

            <div className={'row'}>
                <div className={'col s24 m12'}>
                    {/*{() => this.renderCategories()}*/}


                    {this.props.categories && this.props.categories.map(category => {
                        return <Category category={category} key={category.id}/>
                    })}
                </div>
            </div>
        </div>
    );
}

Hence in your componentWillMount you only need to initiate your request:

componentWillMount() {
    this.props.getCategories();
}

You can also do it in componentDidMount() lifecycle method.

Now when your request resolves and your categories update in the store (Redux) they will be passed again to your component causing it to update. This will also happen with every update in the categories stored in the store.

Also you don't have to call forceUpdate like this unless you have components implementing shouldComponentUpdate lifecycle method and you want them to ignore it and do a force update. You can Read about all these lifecycle methods (and you have to if you are using React) here.

Mohamed Ibrahim Elsayed
  • 2,734
  • 3
  • 23
  • 43