React-native-ui-kitten: Switch/Toggle doesn't update in card component unless scrolled through

Created on 10 Jul 2020  路  2Comments  路  Source: akveo/react-native-ui-kitten

馃挰 Question

Issue: Switch(toogle) component does not update its rendered view (on screen) unless i scroll through the cards/or i press on a card.

Code:

constructor() {
        super();
        this.state = { 

            outputimg:null,
            displayedImages:[],
            pageToken:null,
            nrOfPhotosOnScreen:0,
            pageNames:null,
            authorOfThePost:null,
            visible:false,
            checked:false,
            key:0
        }
    }

This for loop is inside a function that is called onComponentDidMount() but there is no problem with anything else except the switch -->

for(var k=0;k<=Object.keys(this.state.pageNames).length-1;k++){
if(firstPage.items[i].path.replace(/images\//g,'').includes(Object.keys(this.state.pageNames)[k]))
{
    let authorOfThePost = Object.values(this.state.pageNames)[k];
    let linkToPage = "https://www.instagram.com/"+authorOfThePost.replace(/@/g,'')+"/";
    let headerPost = (props) => (
        <View {...props}>
            <Text category='h6'>Posted by </Text><Text style={{color:'blue'}} category='h6' onPress={() => Linking.openURL(linkToPage)}>{authorOfThePost}</Text>
        </View>
    )
    let footerPost = (props) => (
        <View {...props} style={[props.style, styles.footerContainer]}>
            <Toggle checked={this.state.checked} key={this.state.key} onChange={this.onCheckedChange}>
            {`Checked: ${this.state.checked}`}
            </Toggle>
        </View>
    )
    let output = <Card style={styles.card} activeOpacity={1}  footer = {footerPost} header={headerPost}>{outputimg}</Card>
    this.setState({
        displayedImages: [...this.state.displayedImages, output],
        key:this.state.key+1
    });
}

onCheckedChange function -->

    onCheckedChange = () => {
        this.setState({checked:!this.state.checked})
  };

The switch button and the card are imported from UI Kitten( Switch Toggle and Card Component)

How the app looks

UI Kitten and Eva version

| Package | Version |
| ----------- | ----------- |
| @eva-design/eva | ^2.0.0 |
| @ui-kitten/components | ^5.0.0 |

Help wanted

Most helpful comment

There are way too many issues with your code, including performance penalties, and stalled state variables.
The latter one is the issue you are experiencing. It is due to you saving components in state variables. Do not do that.
More specifically, in your case, the Toggle component uses this.state.checked, which at the time of setting the component in the state (with the this.setState(...) in your for loop in componentDidMount) still has the old value. Moreover, the component that you set in state, is only set at componentDidMount time, and it is not updated when you call the onCheckedChange method.
This is called "stalled state", which simply means that the most recent state is still not the most updated state.

The reason why it updates when you scroll over it is because something causes a re-render either in the Toggle component or your whole component, which causes it to re-read the this.state.checked variable - but stalled state issues usually cause undefined behaviors.

I will try to give you an example of better code to use, but keep in mind that this code is incomplete since I don't have your full code. You need to add missing code parts in the proper places.

class Post extends React.Component{
    constructor(props) {
        super(props);

        this.state = {
            outputimg: null,
            displayedImages: [],
            pageToken: null,
            nrOfPhotosOnScreen: 0,
            pageNames: null,
            authorOfThePost: null,
            visible: false,
            checked: false,
            key: 0
        }
    }

    componentDidMount = () => {
        // Do not loop through an array with a conditional for finding a single value
        // The Array.find() method works great in this case
        const pageNamesList = Object.keys(this.state.pageNames);
        const pageName = pageNamesList.find((_pageName) => {
            // Not sure where `firstPage` or `i` comes from
            // it did not appear in the code you posted
            // make sure you update it here as you need
            return (firstPage.items[i].path.replace(/images\//g, '').includes(_pageName));
        });

        // If the Array.find() method cannot find a result, returns undefined
        if (pageName !== undefined) {
            // Now you have your page name here, evaluate values that you need
            const authorOfThePost = this.state.pageNames[pageName];
            const linkToPage = "https://www.instagram.com/" + authorOfThePost.replace(/@/g, '') + "/";

            const data = { authorOfThePost, linkToPage };

            this.setState({
                displayedImages: [
                    ...this.state.displayedImages,
                    data    // append the newly evaluated data
                ]
            });
        }
    }

    onCheckedChange = () => {
        // Try not to use the `!` (not) operator to toggle booleans
        this.setState({
            checked: (this.state.checked === false)
        });
    }

    headerPost = (props) => (
        <View {...props}>
            <Text category='h6'>Posted by </Text><Text style={{color:'blue'}} category='h6' onPress={() => Linking.openURL(linkToPage)}>{authorOfThePost}</Text>
        </View>
    )

    footerPost = (props) => (
        <View {...props} style={[props.style, styles.footerContainer]}>
            <Toggle checked={this.state.checked} onChange={this.onCheckedChange}>
                {`Checked: ${this.state.checked}`}
            </Toggle>
        </View>
    );

    render = () => {
        return this.state.displayedImages.map((data) => {
            return <Card
                // Do not save `key`s in state, and do not use `index` or counting as keys
                // Any string value can be a valid `key`, just make sure it's unique
                // The `linkToPage` string should be unique enough
                key={`${data.linkToPage}`}
                style={styles.card}
                activeOpacity={1}
                footer={this.footerPost}
                header={this.headerPost}
            >
                {/* not sure where `outputimg` comes from */}
                {outputimg}
            </Card>;
        });
    }
}

All 2 comments

There are way too many issues with your code, including performance penalties, and stalled state variables.
The latter one is the issue you are experiencing. It is due to you saving components in state variables. Do not do that.
More specifically, in your case, the Toggle component uses this.state.checked, which at the time of setting the component in the state (with the this.setState(...) in your for loop in componentDidMount) still has the old value. Moreover, the component that you set in state, is only set at componentDidMount time, and it is not updated when you call the onCheckedChange method.
This is called "stalled state", which simply means that the most recent state is still not the most updated state.

The reason why it updates when you scroll over it is because something causes a re-render either in the Toggle component or your whole component, which causes it to re-read the this.state.checked variable - but stalled state issues usually cause undefined behaviors.

I will try to give you an example of better code to use, but keep in mind that this code is incomplete since I don't have your full code. You need to add missing code parts in the proper places.

class Post extends React.Component{
    constructor(props) {
        super(props);

        this.state = {
            outputimg: null,
            displayedImages: [],
            pageToken: null,
            nrOfPhotosOnScreen: 0,
            pageNames: null,
            authorOfThePost: null,
            visible: false,
            checked: false,
            key: 0
        }
    }

    componentDidMount = () => {
        // Do not loop through an array with a conditional for finding a single value
        // The Array.find() method works great in this case
        const pageNamesList = Object.keys(this.state.pageNames);
        const pageName = pageNamesList.find((_pageName) => {
            // Not sure where `firstPage` or `i` comes from
            // it did not appear in the code you posted
            // make sure you update it here as you need
            return (firstPage.items[i].path.replace(/images\//g, '').includes(_pageName));
        });

        // If the Array.find() method cannot find a result, returns undefined
        if (pageName !== undefined) {
            // Now you have your page name here, evaluate values that you need
            const authorOfThePost = this.state.pageNames[pageName];
            const linkToPage = "https://www.instagram.com/" + authorOfThePost.replace(/@/g, '') + "/";

            const data = { authorOfThePost, linkToPage };

            this.setState({
                displayedImages: [
                    ...this.state.displayedImages,
                    data    // append the newly evaluated data
                ]
            });
        }
    }

    onCheckedChange = () => {
        // Try not to use the `!` (not) operator to toggle booleans
        this.setState({
            checked: (this.state.checked === false)
        });
    }

    headerPost = (props) => (
        <View {...props}>
            <Text category='h6'>Posted by </Text><Text style={{color:'blue'}} category='h6' onPress={() => Linking.openURL(linkToPage)}>{authorOfThePost}</Text>
        </View>
    )

    footerPost = (props) => (
        <View {...props} style={[props.style, styles.footerContainer]}>
            <Toggle checked={this.state.checked} onChange={this.onCheckedChange}>
                {`Checked: ${this.state.checked}`}
            </Toggle>
        </View>
    );

    render = () => {
        return this.state.displayedImages.map((data) => {
            return <Card
                // Do not save `key`s in state, and do not use `index` or counting as keys
                // Any string value can be a valid `key`, just make sure it's unique
                // The `linkToPage` string should be unique enough
                key={`${data.linkToPage}`}
                style={styles.card}
                activeOpacity={1}
                footer={this.footerPost}
                header={this.headerPost}
            >
                {/* not sure where `outputimg` comes from */}
                {outputimg}
            </Card>;
        });
    }
}

Thank you so much, after an hour of modifying some of the information you provided I made it work!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

evangunawan picture evangunawan  路  3Comments

sovannvin picture sovannvin  路  3Comments

domsterthebot picture domsterthebot  路  3Comments

bkwhite picture bkwhite  路  3Comments

sarmadkung picture sarmadkung  路  3Comments