0

It's a bit of a wordy title. What I am saying is I have a child component (which is a function component) and also a parent class. However this parent class does not return JSX therefore meaning I can't use hooks / useRef(). I also can't just add a ref to the child component since I get this error:

Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Here's the child class with a bit of the code stripped out:

const BluetoothDeviceItem = (Props) => {
    const [connectingState, setConnectingState] = useState(Props.state());
    useEffect(() => {
        stateChangeCallback();
    }, [connectingState]);

    const { onClick } = Props;

    const stateChangeCallback = () => {
        switch (connectingState) {
            case 'not_connected':
                //stuff
                break;
            case 'connecting':
                //stuff
                break;
            case 'connected':
                //stuff
                break;
        }
    }

    const rerender = () => {
        setConnectingState(Props.state());
    }

    const wait = async () => {
        onClick(Props.mac);
        rerender();

    }

    return (
        <IonItem lines="none" class="item-container" id={Props.mac} onClick={wait} ref={Props.elRef}>
            <IonAvatar slot="start" class="bluetooth-icon-container">
                <IonIcon icon={bluetoothOutline} class="bluetooth-icon"></IonIcon>
            </IonAvatar>
            <IonAvatar slot="end" class="connecting-icons-container">
                <IonIcon icon={closeOutline} class="connecting-icons"></IonIcon>
                <IonSpinner name="dots" class="connecting-spinner"></IonSpinner>
            </IonAvatar>
            <IonLabel>
                <div className="device-name">{Props.name}</div>
                <div className="device-mac">{Props.mac}</div>
            </IonLabel>
        </IonItem>
    );
}
export default BluetoothDeviceItem;

and here's the Parent (again, cleaned a little bit):

const _Bluetooth = () => {

    let states: {} = {}
    const deviceItems: any[] = [];
    const deviceRefs: any[] = [];

    const bluetoothInitialize = () => {
        for (let i = 0; i < 5; i++) {
            let a = {name: "test_"+i, mac: i.toString(), connected: false}
            createDeviceItem(a);
        }
    }

    const connect = (id) => {
        deviceItems.forEach(item => { //disconnecting any other currently connected devices and changing their respective states
            if ((item.props.state() === 'connecting' || item.props.state() === 'connected') && item.props.mac !== id) {
                updateDeviceItemState(item.props.mac, 'not_connected');
            }

            if(id === item.props.mac) {
                updateDeviceItemState(item.props.mac, 'connecting');
            }
        });
    }

    const createDeviceItem = (data) => {
        states = {...states, [data.mac]: data.connected ? 'connected' : 'not_connected'}
        let ref = createRef();
        deviceItems.push(<BluetoothDeviceItem elRef={ref} key={data.mac} mac={data.mac} name={data.name} onClick={(id) => connect(id)} state={() => {return states[data.mac]}} elRef={ref}></BluetoothDeviceItem>);
        deviceRefs.push(ref);
    }
    const updateDeviceItemState = (id, connectingState) => {
        states[id] = connectingState;
    }

    return (
        {deviceItems, bluetoothInitialize}
    );
}
export default _Bluetooth;

The parent class does a Bluetooth scan (for now just replaced with a for loop). Each time it finds a device it creates a new BluetoothDeviceItem. When you click on one, it's state is set to not_connected. At the same time, it checks to see if any other devices are currently connecting or connected.
Now, setting the device to connecting works fine because when you click on it it calls the wait function which updates the state of the BluetoothDeviceItem. However, when updating the state of all the other devices (after checking if they are connected or connecting) because they are not called via the onClick, they don't realise their state was updated. This is why I implemented a rerender function which will be called on every BluetoothDeviceItem to make sure they have the latest state.
The issue with this is that I need to lift this function up so the parent class can access it. The most obvious idea would be to use refs. Create a ref, attach to the BluetoothDeviceItem component and get the function. Of course doing that creates the error I said above.
The next best thing is to pass the ref as a props and attach it to the DOM element. Unfortunately this means I am not able to access the functions since they do not exist on the DOM element.
The last thing I tried was using the spread operator (...) to spread the rerender function like so: <IonItem lines="none" class="item-container" id={Props.mac} onClick={wait} ref={Props.elRef}> {...rerender} so I could access it using the ref. This doesn't work since ...rerender will evaluate to {} (at some point in time I was able to spread a function out but I can't remember how).

How can I lift this rerender function up to the parent or update the BluetoothDeviceItem when its state changes?

This is similar to my previous post but I am actually using the answer provided and storing all states in the parent class. I think just due to the nature of how I need these classes and components to work there will always be a need to access something from the child component in the parent class while also having strict restrictions like the child being a function component or the parent not returning JSX

DreamingInsanity
  • 167
  • 2
  • 10

1 Answers1

0

I couldn't really find a simple way of doing it so I ended up just converting the BluetoothDeviceItem function component to a React Component, like so:

class BluetoothDeviceItem extends React.Component<Props, {}> {
    constructor(props) {
        super(props);
    }

    private updateIcons = () => {
        switch (this.props.state()) {
            case 'not_connected':
                break;
            case 'connecting':
                break;
            case 'connected':
                break;
        }
    }

    public rerender = () => {
        this.updateIcons();
    }

    private wait = async () => {
        this.props.onClick(this.props.mac);
        this.rerender();
    }

    render() {
        return (
            <IonItem lines="none" class="item-container" id={this.props.mac} onClick={this.wait}>
                <IonAvatar slot="start" class="bluetooth-icon-container">
                    <IonIcon icon={bluetoothOutline} class="bluetooth-icon"></IonIcon>
                </IonAvatar>
                <IonAvatar slot="end" class="connecting-icons-container">
                    <IonIcon icon={closeOutline} class="connecting-icons"></IonIcon>
                    <IonSpinner name="dots" class="connecting-spinner"></IonSpinner>
                </IonAvatar>
                <IonLabel>
                    <div className="device-name">{this.props.name}</div>
                    <div className="device-mac">{this.props.mac}</div>
                </IonLabel>
            </IonItem>
        );
    }
}

export default BluetoothDeviceItem;

and then just using a ref when I create an instance on that component. That gives me access to the functions I need.

DreamingInsanity
  • 167
  • 2
  • 10