2

I knew this.refs are deprecated and can change it with React.createRef but in my case is different

This is our code

export default class ScrollableTabString extends Component {
    static ITEM_PADDING = 15;
    static defaultProps = {
        tabs: [],
        themeColor: '#ff8800',
    };
    static propTypes = {
        tabs: PropTypes.any,
        themeColor: PropTypes.string,
    };

    shouldComponentUpdate(nextProps, nextState) {
        if (nextProps.tabs !== this.props.tabs || this.state !== nextState) {
            return true;
        }
        return false;
    }

    constructor(props) {
        super(props);
        this.views = [];
        this.state = {};
        this.scrollableTabRef = React.createRef();
    }

    componentDidMount() {
        setTimeout(() => {
            this.select(this.props.defaultSelectTab ? this.props.defaultSelectTab : 0);
        }, 300);
    }

    select(i, code) {
        let key = 'tab ' + i;
        for (let view of this.views) {
            if (view) {
                let isSelected = false;
                if (view.key === key) {
                    isSelected = true;
                }
                // This refs is Object with many ref views
                if (this.refs[view.key]) {
                    this.refs[view.key].select(isSelected);
                }
                this.scrollableTabRef.current.goToIndex(i);
            }
        }
        if (this.props.onSelected) {
            this.props.onSelected(code);
        }
    }

    render() {
        this.views = [];

        if (this.props.tabs) {
            let tabs = this.props.tabs;
            for (let i = 0; i < tabs.length; i++) {
                if (tabs[i]) {
                    let key = 'tab ' + i;
                    let view = (
                        <TabItem
                            column={tabs.length}
                            themeColor={this.props.themeColor}
                            useTabVersion2={this.props.useTabVersion2}
                            isFirstItem={i === 0}
                            isLastItem={i === tabs.length - 1}
                            ref={key}
                            key={key}
                            tabName={tabs[i].name}
                            onPress={() => {
                                this.select(i, tabs[i].code);
                            }}
                        />
                    );
                    this.views.push(view);
                }
            }
        }
        let stypeTab = this.props.useTabVersion2
            ? null
            : { borderBottomWidth: 0.5, borderColor: '#8F8E94' };

        return (
            <ScrollableTab
                ref={this.scrollableTabRef}
                style={Platform.OS === 'ios' ? stypeTab : null}
                contentContainerStyle={Platform.OS === 'android' ? stypeTab : null}
            >
                {this.views}
            </ScrollableTab>
        );
    }
}

I want to fix the warning from eslint but in our case, I can't use React.createRef

Anhdevit
  • 1,926
  • 2
  • 16
  • 29

1 Answers1

2

First Solution

If your code is working fine and just want to suppress eslint warning,

please put this line on first line of your file: (I believe your eslint warning should be react/no-string-refs)

/* eslint react/no-string-refs: 0 */

Second Solution

If your case is you cannot use createRef(), then try to achieve like this:

<ScrollableTab ref={(tab) => this.scrollableTab = tab} ...

And then called like this:

this.scrollableTab?.goToIndex(index);

Simplify Your Code

After reading your sample codes, I suggest you to use state instead of string refs.

Also as there are a lot of redundancy code in your sample, I have tried to simplify it.

import React, { Component } from 'react';
import { Platform } from 'react-native';
import PropTypes from 'prop-types';

class ScrollableTabString extends Component {
    static ITEM_PADDING = 15;
    state = { activeTab: null }; scrollableTab;
    stypeTab = (!this.props.useTabVersion2 ? { borderBottomWidth: 0.5, borderColor: '#8F8E94' } : null);


    shouldComponentUpdate(nextProps, nextState) {
        return (nextProps.tabs !== this.props.tabs || this.state !== nextState);
    }

    componentDidMount() {
        this.setState({ activeTab: this.props.defaultSelectTab || 0 });
    }

    /* If your second param - code is optional, add default value */
    select = (index, code = null) => {
        let key = `tab ${index}`;

        /* Set activeTab state instead of calling function in every views */
        this.setState({ activeTab: key });
        
        this.scrollableTab?.goToIndex(index);
        /* Not sure what are you archiving, but why don't you use component state and pass the isSelected into item */
        /*for (let view of this.views) {
            if (view) {}
        }*/

        this.props.onSelected && this.props.onSelected(code);
    }

    renderTabs = () => {
        const { tabs, themeColor, useTabVersion2 } = this.props;

        return tabs?.map((tab, index) => {
            if (!tab) { return null; }

            return (
                <TabItem 
                    key={`tab ${index}`}
                    column={tabs.length} 
                    themeColor={themeColor} 
                    useTabVersion2={useTabVersion2}
                    isFirstItem={index === 0}
                    isLastItem={index === (tabs.length - 1)}
                    tabName={tab.name}
                    onPress={() => this.select(index, tab.code)}
                    isSelected={this.state.activeTab === `tab ${index}`}
                />
            );
        });
    };

    render() {
        return (
            <ScrollableTab
                ref={(tab) => this.scrollableTab = tab}
                style={Platform.OS === 'ios' ? stypeTab : null}
                contentContainerStyle={Platform.OS === 'android' ? stypeTab : null}
            >
                {this.renderTabs()}
            </ScrollableTab>
        );
    }
}

ScrollableTabString.defaultProps = {
    onSelected: null,  /* Miss this props */
    tabs: [],
    themeColor: '#ff8800',
    useTabVersion2: false    /* Miss this props */
};

ScrollableTabString.propTypes = {
    onSelected: PropTypes.func,  /* Miss this props */
    tabs: PropTypes.any,
    themeColor: PropTypes.string,
    useTabVersion2: PropTypes.bool  /* Miss this props */
};

export default ScrollableTabString;

Update (18-02-2021)

Using select() with TabItem may leads to several issues:

  • Performance issues (When user press one TabItem, select() will be called as many times as number of TabItems, to change every state inside)
  • Poorly coding styles and maintenance costs as too many duplicate states and refs

I strongly suggest to set state inside parent component and pass it as props to child. To explain clearly how it works, here is an simplest sample for you:

ScrollableTabString.js

class ScrollableTabString extends Component {
  state = { activeTab: null };

  selectTab = (tabName) => { this.setState({ activeTab: tabName }); };

  renderTabs = () => this.props.tabs?.map((tab, index) => (
    <TabItem key={`tab ${index`} tabName={tab.name} onPress={this.selectTab} activeTab={this.state.activeTab} />
  ));
}

TabItem.js

class TabItem extends Component {
  /* Use this function on Touchable component for user to press */
  onTabPress = () => this.props.onPress(this.props.tabName);

  render() {
    const { activeTab, tabName } = this.props;
    const isThisTabActive = (activeTab === tabName);

    /* Use any props and functions you have to achieve UI, the UI will change according to parent state */
    return (
      <TouchableOpacity onPress={this.onTabPress} style={isThisTabActive ? styles.activeTab : styles.normalTab}>
        <Text>{tabName}</Text>
      </TouchableOpacity>
    );
  }
}

If you do really have some state needed to be change inside TabItem, try using componentDidUpdate() in there, but it should be also a redundancy codes:

componentDidUpdate(prevProps) {
  if (this.props.activeTab !== prevProps.activeTab) {
     // change your state here
  }
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Wing Choy
  • 900
  • 10
  • 25
  • I agree with the first solution but in my code the problem with this line ``` // This refs is Object with many ref views if (this.refs[view.key]) { this.refs[view.key].select(isSelected); } ``` – Anhdevit Feb 17 '21 at 17:12
  • 1
    Hi @Anhdevit, correct me if I'm wrong. I think you are trying to set activeTab when user click on the tabItem inside scrollableTabString. If that's the case, I think you should try using state to handle the activeTab instead of looping the value in select() function. Please try the sample code I have written in the answer, thanks. – Wing Choy Feb 18 '21 at 01:56
  • I think you are right but with my case select still right because it set the state insite TabItem. Can I still use the select function with TabItem? – Anhdevit Feb 18 '21 at 02:28
  • 1
    Hi @Anhdevit, the states inside every TabItem are actually connected to the same data. I have made some updates on the answer for you to have an easier understanding. If you have any reason that must need to use select(), please try to make another question with full example, thank you :) – Wing Choy Feb 18 '21 at 03:53