1

The following is a first attempt at learning to simply change the style of an element onPress in react native. Being well versed in web languages I am finding it difficult as it is not as straight forward.

For reasons as yet unknown, the element requires two clicks in order to execute.

export class NavTabItem extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            active: false
        }
        this.NavTabAction = this.NavTabAction.bind(this)
    }

    NavTabAction = (elem) => {
        elem.setState({active: !elem.state.active})
    }

    render() {
        return (
            <TouchableOpacity
            style={this.state.active ? styles.NavTabItemSelected : styles.NavTabItem}
            onPress={()=> {
                this.NavTabAction(this)
            }}>
                <View style={styles.NavTabIcon} />
                <Text style={styles.NavTabLabel}>{this.props.children}</Text>
            </TouchableOpacity>
     );
  }
}

Other issues:

I also have not worked out how a means of setting the active state to false for other elements under the parent on click.

Additionally, Is there a simple way to affect the style of child elements like with the web. At the moment I cannot see a means of a parent style affecting a child element through selectors like you can with CSS

eg. a stylesheet that read NavTabItemSelected Text :{ // active style for <Text> }

Walrus
  • 19,801
  • 35
  • 121
  • 199
  • clarify what exactly requires two clicks. What is the expected result? Elements don't execute. What functions are being called on first and second click? – Max Feb 26 '20 at 13:11
  • @Max the NavTabAction function is being fired on any click, however the state does not change until a second click is made. – Walrus Feb 26 '20 at 13:17
  • Is it requiring 2 clicks? could you please prepare an expo snack to see it? – Ian Vasco Feb 26 '20 at 13:21
  • how do you check for state not being changed? Have you tried `console.log(this.state.active)` in render? @RobinKnight – Max Feb 26 '20 at 13:24
  • @Max yes and you can see it is false on the first click and true on the second click – Walrus Feb 26 '20 at 13:25
  • if it's false on (after?) first click, what is it initially then? are you sure you didn't make a naming mistake and it's not initially undefined? @RobinKnight – Max Feb 26 '20 at 14:02
  • @Max yes, I'm logging it both before and after setState and its false both before and after on the first click. It's them both true before and after on second click and false before and after on third click etc.. This makes no sense. – Walrus Feb 26 '20 at 14:10
  • @Max I tried this as follows. How can it be false before and after on click one and true before and after on click 2? NavTabAction = () => { console.log('before: ' + this.state.active) if(this.state.active === false){ this.setState({active: true}) } console.log('after: ' + this.state.active) } – Walrus Feb 26 '20 at 14:13
  • 1
    `setState` is asynchronous https://reactjs.org/docs/react-component.html#setstate that's why I specifically asked about console.logs in render @RobinKnight – Max Feb 26 '20 at 14:40
  • @Max Right. Sorry. Console log is correct in render(). The problem is therefore the ternary operator which is showing 'styles.NavTabItemSelected' when the console logs false instead of true. Sorry about the mess, I am new to this, but why is this the case? – Walrus Feb 26 '20 at 14:50
  • 1
    according to code provided it should apply styles.NavTabItemSelected styles if this.state.active is truthy, so I'd say check your styles.. I've never head of ternary operators not working – Max Feb 26 '20 at 15:13
  • @Max agreed however that is not it. I have now removed the ternary and replaced it with an if / else on the render return and its exactly the same behaviour. It's as though the content is returned before the set state even though a console log in render shows this not to be the case. I have no idea what is going on. – Walrus Feb 26 '20 at 15:24
  • The bit before render works as expected however the ternary in style inside the return() does not. I am at a loss. render() { console.log('render1: ' + this.state.active) this.state.active == true ? console.log("selected") : console.log("unselected") return ( {this.props.children} ); } – Walrus Feb 26 '20 at 15:36

2 Answers2

1

Instead of calling elem.setState or elem.state, it should be this.setState and elem.state.

NavTabAction = (elem) => {
    this.setState(prev => ({...prev, active: !prev.active}))
}

And instead of passing this in the onPress, you should just pass the function's reference.

onPress={this.NavTabAction}>

You should also remove this line because you are using arrow function

// no need to bind when using arrow functions
this.NavTabAction = this.NavTabAction.bind(this)

Additionally, Is there a simple way to affect the style of child elements like with the web

You could check styled-component, but I think that feature don't exists yet for react native. What you should do is pass props down to child components.

Vencovsky
  • 28,550
  • 17
  • 109
  • 176
  • Thanks however it is still requiring two click. – Walrus Feb 26 '20 at 13:06
  • 1
    Having seperate stylesheets and conditionaly asking for a state and also setting this state correctly should trigger a rerender of the screen/or elements that get changed. – yesIamFaded Feb 26 '20 at 13:12
  • @yesIamFaded it does but only on the second click of each element – Walrus Feb 26 '20 at 13:22
  • @RobinKnight then I guess its an error in your onPress function try to add a console.log to it and look if it fires on the first click. You can also try to set the onPress to: ` onPress={()=> this.setState({ active: !this.state.active }) ` or something like this – yesIamFaded Feb 26 '20 at 13:26
  • @yesIamFaded Same behaviour and the console shows the status as false on the first click. – Walrus Feb 26 '20 at 13:28
  • @yesIamFaded to clarify, it is an extra click that is required only on the first interaction. so the first onPress reads active state of 'false' on the first click instead of true. After that on click will change it to 'true' and another to 'false' etc.. its just this first click that reads false instead of true. – Walrus Feb 26 '20 at 13:35
  • @RobinKnight I have made some changes, please try it – Vencovsky Feb 26 '20 at 13:36
  • @Vencovsky already tried your changes. No change. The first onPress first false instead of true for reasons unknown. PS. thanks very much for help. – Walrus Feb 26 '20 at 13:39
0

Thanks to everyone for their help with this and sorting out some other bits and pieces with the code.

The issue in question however was that the style was changing on the second click. A few hours later and I have a cause and a solution for anyone suffering from this. Should any of the far more experienced people who have answered this question believe this answer is incorrect or they have a better one, please post it but for now here is the only way I have found to fix it.

The cause:

Using setState was correctly re rendering the variables. This could both be seen in the console via console.log() and directly outputted in the render making them visible.

However, no matter what was tried, this did not update the style. Whether it was a style name from the Stylesheet or inline styles, they would update on the second click rather than the first but still to the parameters of the first. So if the first click should make a button turn from red to green, it would not do so even though the new state had rendered. However if a subsequent click should have turned the button back to red then the button would now go green (like it should have for the first click). It would then go red on the third click seemingly always one step behind the status passed to it.

Solution

To fix this, take the style off the the primary element (forgive terminology, someone edit), in my case, the TouchableOpacity element. Add in a child View element and place the styles on that View element instead along with the ternary operator and wallah.

It seems any change to status on the effective master element or container if you prefer, only takes affect after another render, not that contained in setStatus.

Final code:

export class NavTabItem extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            active: false
        }
    }

    NavTabAction = () => {
        this.setState({active: !this.state.active})
    }

    render() {
        this.state.active == true ? console.log("selected") : console.log("unselected")
        return (
            <TouchableOpacity onPress={this.NavTabAction}>

              // added View containing style and ternary operator

                <View style={this.state.active == true ? styles.NavTabItemSelected : styles.NavTabItem}>
                    <View style={styles.NavTabIcon} />
                    <TextCap11 style={styles.NavTabLabel}>{this.props.children}</TextCap11>
                </View>

              // End added view

            </TouchableOpacity>
     );
  }
}
Walrus
  • 19,801
  • 35
  • 121
  • 199
  • 1
    I've never experienced effects you describe (elements applying wrong styles because of being wrappers to other elements). It should be of course possible to use styles on wrapper elements without creating extra children. I believe minimal reproducible example is the only way to properly dissect the problem – Max Feb 26 '20 at 20:56
  • @Max I'lll set up a snack when I have a moment on. – Walrus Feb 27 '20 at 11:59