Ata Mohammadi
Ata Mohammadi

Reputation: 3530

React Native: Props changed, component didn't re-render

After an action, props are being changed. componentWillUpdate also triggers but, component is not re-rendering.

See the code :

import React, {Component, PropTypes} from "react";
import {
    ActivityIndicator,
  Text,
  View,
  Image,
  NetInfo,
  Alert,
  TouchableOpacity,
  ScrollView,
  TextInput,
  Dimensions,
    RefreshControl,
  Platform
} from 'react-native';
import { styles, moment, GoogleAnalytics, KeyboardAwareScrollView, DeviceInfo, Loader, Accordion, I18n, CustomNavBar, DatePicker, FMPicker, CustomStarRating, Icon, CustomPicker, CustomQuestion, CustomDatePicker } from "../../common/components";
let { width, height } = Dimensions.get('window');
GoogleAnalytics.setTrackerId('UA-86421142-1');
GoogleAnalytics.trackScreenView('Evaluation Page');
GoogleAnalytics.setDispatchInterval(5);
var index = 0;

export default class Evaluation extends Component {
    static propTypes = {
        user: PropTypes.string.isRequired,
    users: PropTypes.object.isRequired,
        evaluation: PropTypes.object.isRequired,
    getEvaluation: PropTypes.func.isRequired,
        submitEvaluation: PropTypes.func.isRequired
    };

    constructor(props) {
    super(props);
    this.state = {
            isLoading: true,
            evaluationList: '',
            totalEval: 0,
            date: moment().format('DD-MM-YYYY'),
            isRefreshing: false
    };
        this.list = {};
  }

  componentDidMount(){
    this.loadData();
        NetInfo.isConnected.fetch().then(isConnected => {
      this.setState({
        isConnected: isConnected
      });
    });
    NetInfo.isConnected.addEventListener(
      'change',
      isConnected => {
        this.setState({
          isConnected: isConnected
        });
      }
    );
  }

    shouldComponentUpdate(nextProps, nextState){
        let shouldUpdate = false;
        const oldValue = this.props.evaluation[this.props.user];
        const newValue = nextProps.evaluation[nextProps.user];
        Object.keys(newValue).forEach((index)=>{
            if(!oldValue.hasOwnProperty(index)){
                shouldUpdate = true;
            }
        });
        Object.keys(oldValue).forEach((index)=>{
            if(!newValue.hasOwnProperty(index)){
                shouldUpdate = true;
            }
        });
        console.log('should component update?', shouldUpdate);
        return shouldUpdate;
    }

    randomNumber(){
    index++;
    return index;
  }

  async loadData(cb = ()=>{}){
        await this.props.getEvaluation(this.props.users);
    let {user, users, evaluation, getEvaluation} = this.props;
    let data = evaluation[user];
    let dsource = [];
        let list = {};
    Object.keys(data).forEach((e)=>{
            let currentEvaluation = data[e];
            let fields = [];
            list[currentEvaluation.evaluationId] = {};
            currentEvaluation.evaluationField.forEach((f)=>{
                fields.push({
                    ...f,
                    value: ''
                });
                list[currentEvaluation.evaluationId][f.field_name] = {
                    value: '',
                    required: f.required
                };
            });
      dsource.push({
                id: currentEvaluation.evaluationId,
                title: currentEvaluation.evaluationTitle,
                expire: currentEvaluation.evaluationExpire,
                image: currentEvaluation.evaluationImage,
                count: currentEvaluation.evaluationField.length,
                fields: fields
      });
    });
        this.list = list;
    this.setState({
            evaluationList: dsource,
            isLoading: false,
            totalEval: dsource.length,
    });
        this.forceUpdate();
        cb();
  }

    async getObjectToPost(evaluationID){
        let obj = this.list;
    return obj[evaluationID];
  }

    async changeValue(a,b,c,type){
        let list = this.list;
    if(type == 'date' || type == 'picker'){
      list[a][b].value = c;
    } else {
      let oldValue = this.getValue(a,b);
      if(oldValue != c){
                list[a][b].value = c;
            }
    }
        this.list = list;
  }

  async getValue(id, name){
        let list = this.list;
        return list[id][name].value;
  }

    async startEvaluationSubmission(user, users, id, data, cb){
        await cb(user, users, id, data, ()=>{
            Alert.alert(
                I18n.t("evaluation_submitted_title"),
                I18n.t("evaluation_submitted_desc")
            );
        });
    }

    async submitEvaluation(evalid){
        const {user, users, evaluation, getEvaluation, submitEvaluation} = this.props;
    let allRequiredEnetered = true;
    let objToPost = {};
    let answers = await this.getObjectToPost(evalid);
    for(let key in answers){
      objToPost[key]=answers[key].value;
      if(answers[key].required == true && answers[key].value == ''){
        allRequiredEnetered = false;
      }
    }
    if(allRequiredEnetered){

      objToPost = {
        result: objToPost
      };
      let stringifiedObject = JSON.stringify(objToPost);

      if(this.state.isConnected){
                this.startEvaluationSubmission(user, users, evalid, stringifiedObject, submitEvaluation);
      } else {
                //// Save evaluation to submit later.
        Alert.alert(
          I18n.t("offline_mode_title"),
          I18n.t("evaluation_offline")
        );
      }

    } else {
      Alert.alert(
        I18n.t("invalid_input_title"),
        I18n.t("please_fill_in")
      );
    }
  }

    renderQuestions(EvaluationFields,TotalEvaluationsCount,EvaluationID){ // evald.fields, evald.count, evald.id
    let tmdata = [];
    for(let n=0; n < TotalEvaluationsCount; n++){
      if(n > 0){
        tmdata.push(
        <View key={this.randomNumber()} style={styles.separator}></View>
        );
      }
      tmdata.push(
        <Text key={this.randomNumber()} style={styles.questionTitle} >{EvaluationFields[n].label}{EvaluationFields[n].required > 0 ? ' *' : ''}{'\n'}</Text>
      );
      switch (EvaluationFields[n].type) {
        case 'date':
        let currentValue = this.getValue(EvaluationID, EvaluationFields[n].field_name);
        let dateToShow = this.props.date;
        if(currentValue.length != undefined && currentValue.length != ''){
          dateToShow = currentValue;
        }
        tmdata.push(
          <View style={styles.datepicker} key={this.randomNumber()}>
          <CustomDatePicker
            mode="date"
            placeholder={I18n.t("select_date")}
            format="DD-MM-YYYY"
            minDate="01-01-2000"
            maxDate="01-01-2099"
            showIcon={false}
            confirmBtnText={I18n.t("confirm_button")}
            cancelBtnText={I18n.t("login_page_scan_cancel")}
            onDateChange={(date) => {this.changeValue(EvaluationID, EvaluationFields[n].field_name, date, 'date');}}
            required={EvaluationFields[n].required > 0 ? true : false}
          />
          </View>
        );
        break;
        case 'text':
        tmdata.push(
          <TextInput
            key={this.randomNumber()}
            style={[styles.textinput, Platform.OS == "android" ? { borderWidth: 0, height: 35 } : {}]}
            onChangeText={(text) => {this.changeValue(EvaluationID, EvaluationFields[n].field_name, text, 'text');}}
            maxLength = {Number(EvaluationFields[n].max_length)}
            autoCorrect={false}
            autoCapitalize={'none'}
            clearButtonMode={'always'}
            placeholder={I18n.t("evaluations_comment_field")}
          />
        );
        break;
        case 'rate':
        tmdata.push(
          <View key={this.randomNumber()} style={styles.starrating}>
            <CustomStarRating
              maxStars={Number(EvaluationFields[n].stars)}
              rating={Number(this.getValue(EvaluationID, EvaluationFields[n].field_name))}
              selectedStar={(rating) => {this.changeValue(EvaluationID, EvaluationFields[n].field_name, rating, 'rating');}}
              starSize={(width / (Number(EvaluationFields[n].stars))) > ( width / 10) ? ( width / 10) : (width / (Number(EvaluationFields[n].stars)))}
              required={EvaluationFields[n].required > 0 ? true : false}
            />
          </View>
        );
        break;
      }
      if(EvaluationFields[n].type == 'list'){
        if(EvaluationFields[n].widget == 'note'){
          tmdata.push(
            <View key={this.randomNumber()}>
              <CustomQuestion
                  evaluationId={EvaluationID}
                  fieldName={EvaluationFields[n].field_name}
                  allowedValues={EvaluationFields[n].allowed_values}
                  noteColors={EvaluationFields[n].note_colors}
                  onChange={(value)=>{ this.changeValue(EvaluationID, EvaluationFields[n].field_name, value, 'custom') }}
                  required={EvaluationFields[n].required > 0 ? true : false}
              />
            </View>
          );
        } else {
          let allowedValues = EvaluationFields[n].allowed_values;
          let Options=[];
          let LabelsForOptions=[];

          for(let r=0; r < allowedValues.length; r++){
            Options.push(allowedValues[r][0]);
            LabelsForOptions.push(allowedValues[r][1]);
          }
          tmdata.push(
            <View style={Platform.OS == "ios" ? styles.picker : styles.pickerSimple} key={this.randomNumber()}>
              <CustomPicker
                options={Options}
                labels={LabelsForOptions}
                onSubmit={(option) => {this.changeValue(EvaluationID, EvaluationFields[n].field_name, option, 'picker');}}
                confirmBtnText={I18n.t("confirm_button")}
                cancelBtnText={I18n.t("login_page_scan_cancel")}
                text={I18n.t("please_select_answer")}
                required={EvaluationFields[n].required > 0 ? true : false}
              />
            </View>
          );
        }
      }
    }
    return(
      <View key={this.randomNumber()}>{tmdata}</View>
    );
  }

    renderRow() {
        if(!this.state.isLoading){
    let eval_length = this.state.totalEval;
    let content = [];
    let evaluationList = this.state.evaluationList;

    for(let x=0; x < eval_length; x++){
      let evald = evaluationList[x];

      content.push(
        <View style={[styles.cardContainer, (x+1) == eval_length ? { marginBottom: 6 } : {}]} key={this.randomNumber()}>
          <View style={styles.cardHeader} >
            <View style={styles.headerImageContainer}>
              <Image style={styles.headerImage} source={{uri: evald.image}} />
            </View>
            <View style={{ margin: 5 }}>
              <Text style={styles.cardTitle}>{evald.title}</Text>
            </View>
          </View>
          <View style={{ padding: 5 }}>
            {this.renderQuestions(evald.fields, evald.count, evald.id)}
          </View>
          <View style={{ padding: 5 }}>
            <View style={styles.separator}></View>
            <Text style={styles.footerText}>{I18n.t("evaluations_mandatory")}{'\n'}{I18n.t("evaluations_desc_expire")} {evald.expire}</Text>
            <TouchableOpacity onPress={() => this.submitEvaluation(evald.id)} style={styles.submitButton} >
              <Text style={styles.buttonText}>{I18n.t("evaluations_submit_button")}</Text>
            </TouchableOpacity>
          </View>
        </View>
      );
    }
      return(
        <View>
          <KeyboardAwareScrollView>
            <View key={this.randomNumber()}>
              {content}
            </View>
          </KeyboardAwareScrollView>
        </View>
      );
    }
  }

    renderData(){
        if(this.state.totalEval > 0){
      return(
                <View style={styles.container} key={this.randomNumber()}>
            {this.renderRow()}
        </View>
      );
    } else {
      return(
        <View style={styles.errorContainer}>
          <View style={styles.error}>
            <Text style={styles.Errortext}>
              {I18n.t("evaluations_no_evaluation_available")}
            </Text>
          </View>
        </View>
      );
    }
  }

    render() {
        const {user, users, evaluation, getEvaluation} = this.props;
        return (
        <View style={styles.container}>
          <View style={{ width: width, height: Platform.OS == "ios" ? 64 : 54}}>
            <CustomNavBar
              width={width}
              height={Platform.OS == "ios" ? 64 : 54}
              title={I18n.t("evaluation_page_nav_title")}
              titleSize={18}
              buttonSize={15}
              background={"#00a2dd"}
              color={"#FFF"}
              rightIcon={"ios-person-outline"}
              rightIconSize={30}
              rightAction={()=> { this.props.openProfile(); }}
            />
          </View>
          <View style={{ height: Platform.OS == "ios" ? height - 114 : height - 130 }}>
            {!this.state.isLoading ?
              <ScrollView
              refreshControl={
                <RefreshControl
                  refreshing={this.state.isRefreshing}
                  onRefresh={this.loadData.bind(this)}
                  tintColor="#00a2dd"
                  title=""
                  titleColor="#00a2dd"
                  colors={['#00a2dd', '#00a2dd', '#00a2dd']}
                  progressBackgroundColor="#FFFFFF"
                />
              }
            >
              {this.renderData()}
            </ScrollView>
            :<ActivityIndicator
                        animating={true}
                        style={{ paddingTop: Platform.OS == "ios" ? (height - 114)/2 : (height - 130)/2 }}
                        color={'#00a2dd'}
                        size={'small'}
                    />}
          </View>
        </View>
    );
    }
}

Console log output :

Console log output

Any solutions please?


UPDATE : Whole code have been added to the question.

After evaluation submission, props changes. the submitted evaluation will be removed from the evaluation list, but it will be still rendered. calling the loadData() through RefreshControl (Pulldown to refresh) will re-render correctly and the evaluation will be removed.

Thanks in advance.

Upvotes: 2

Views: 2396

Answers (1)

jonashaefele
jonashaefele

Reputation: 71

I ran into a similar problem, where props wouldn't trickle down to list items I rendered form a parent account. So I had a look at your code.

What currently happens for you is the following:

  • You initially call this.loadData(); in componentDidMount
  • As your props change, shouldComponentUpdate returns true if any of the new user details didn't exist in the old user object or if any that did in the old user object exist are missing in the new one.
  • if shouldComponentUpdate returns true, that will trigger componentWillUpdate and eventually render

componentWillUpdate() isn't defined in your code, and componentDidMount only gets called once. So your component doesn't know fetch the new data and therefore render won't have the new data to display.

Short answer

Without really knowing more about your app, I'd try adding a componentWillUpdate function that calls this.loadData which will refresh your state and trigger a re-render with forceUpdate on the last line of your loadData function.

Maybe check https://facebook.github.io/react/docs/react-component.html to see which functions are triggered in the component lifecycle.

Long answer

However to make your app easier to maintain and probably faster as well, you might want to consider splitting the data fetching and the rendering into different components.

Since you're already using redux, all the data fetching should preferably be handled in redux. So you'd have something the following:

Redux Reducer

  • with functions like getEvaluation() and startEvaluationSubmission() to load/save evaluations to the server...
  • providing state.evaluations

EvaluationsContainer.jsx

  • connecting to the state and calling the functions like getEvaluation(userID)
  • linking state.evaluations to it's props (with redux connect) and then rendering EvaluationsList.jsx with just the evaluations it needs to render.

EvaluationsList.jsx

  • gets it's own (and only that one) evaluation to display as a prop from EvaluationContainer.jsx

This way, if any of the data changes in the store, the container will automatically trigger an update of your container and trickle down to the display component. You probably won't need to override react's 'shouldComponentUpdate` function, as it's pretty good and really efficient.

Abhi Aiyer wrote up some really nice articles on redux: https://medium.com/front-end-developers/how-we-redux-part-1-introduction-18a24c3b7efe#.gr289pzbi

especially part 5 could be interesting for you: https://medium.com/modern-user-interfaces/how-we-redux-part-5-components-bddd737022e1#.izwodhwwk

Hope that helps at all... that's all I could see without playing with the code. Let me know how you get on.

Upvotes: 1

Related Questions