Skip to content

feat: addListener returns removal function#70

Merged
mfazekas merged 4 commits into
mainfrom
feat/addListener-returns-function
Dec 11, 2025
Merged

feat: addListener returns removal function#70
mfazekas merged 4 commits into
mainfrom
feat/addListener-returns-function

Conversation

@mfazekas
Copy link
Copy Markdown
Collaborator

@mfazekas mfazekas commented Dec 8, 2025

Summary

  • addListener now returns () => void instead of requiring removeListeners() to clear all
  • Enables granular listener management - remove individual listeners without affecting others
  • Uses weak references internally to prevent memory leaks

Usage

const removeListener = property.addListener((value) => {
  console.log('Value changed:', value);
});

// Later, to remove just this listener:
removeListener();

Test code

Full test component used in RiveDataBindingExample.tsx
import {
  View,
  Text,
  StyleSheet,
  ActivityIndicator,
  Button,
} from 'react-native';
import { useEffect, useMemo, useState, useRef } from 'react';
import {
  Fit,
  RiveView,
  useRiveNumber,
  type ViewModelInstance,
  type RiveFile,
  useRiveString,
  useRiveColor,
  useRiveTrigger,
  useRiveFile,
} from '@rive-app/react-native';
import { type Metadata } from '../helpers/metadata';

export default function WithRiveFile() {
  const { riveFile, isLoading, error } = useRiveFile(
    require('../../assets/rive/rewards.riv')
  );

  return (
    <View style={styles.container}>
      <View style={styles.riveContainer}>
        {isLoading ? (
          <ActivityIndicator size="large" color="#0000ff" />
        ) : riveFile ? (
          <WithViewModelSetup file={riveFile} />
        ) : (
          <Text style={styles.errorText}>{error || 'Unexpected error'}</Text>
        )}
      </View>
    </View>
  );
}

function WithViewModelSetup({ file }: { file: RiveFile }) {
  const viewModel = useMemo(() => file.defaultArtboardViewModel(), [file]);
  const instance = useMemo(
    () => viewModel?.createDefaultInstance(),
    [viewModel]
  );

  if (!instance || !viewModel) {
    return (
      <Text style={styles.errorText}>
        {!viewModel
          ? 'No view model found'
          : 'Failed to create view model instance'}
      </Text>
    );
  }

  return <DataBindingExample instance={instance} file={file} />;
}

function DataBindingExample({
  instance,
  file,
}: {
  instance: ViewModelInstance;
  file: RiveFile;
}) {
  const { error: coinValueError } = useRiveNumber('Coin/Item_Value', instance);

  if (coinValueError) {
    console.error('coinValueError', coinValueError);
  }

  const { setValue: setButtonText } = useRiveString('Button/State_1', instance);

  const { setValue: setBarColor, error: barColorError } = useRiveColor(
    'Energy_Bar/Bar_Color',
    instance
  );

  if (barColorError) {
    console.error('barColorError', barColorError);
  }

  const { error: triggerError } = useRiveTrigger('Button/Pressed', instance, {
    onTrigger: () => {
      console.log('Button pressed');
    },
  });

  if (triggerError) {
    console.error('triggerError', triggerError);
  }

  useEffect(() => {
    setButtonText("Let's go!");
    setBarColor('#0000FF');
  }, [setBarColor, setButtonText]);

  // Direct addListener usage (without hooks)
  const [coinValue, setCoinValue] = useState<number | null>(null);
  const [isListening, setIsListening] = useState(true);
  const removeListenerRef = useRef<(() => void) | null>(null);

  useEffect(() => {
    const coinProperty = instance.numberProperty('Coin/Item_Value');
    if (!coinProperty) return;

    // Add listener and store the remover function
    removeListenerRef.current = coinProperty.addListener((value) => {
      console.log('Coin value changed:', value);
      setCoinValue(value);
    });

    return () => {
      // Clean up on unmount
      removeListenerRef.current?.();
    };
  }, [instance]);

  const toggleListener = () => {
    if (isListening && removeListenerRef.current) {
      // Remove the listener by calling the remover function
      removeListenerRef.current();
      removeListenerRef.current = null;
      setIsListening(false);
    } else if (!isListening) {
      // Re-add the listener
      const coinProperty = instance.numberProperty('Coin/Item_Value');
      if (coinProperty) {
        removeListenerRef.current = coinProperty.addListener((value) => {
          console.log('Coin value changed:', value);
          setCoinValue(value);
        });
        setIsListening(true);
      }
    }
  };

  return (
    <View style={styles.flex}>
      <View style={styles.listenerDemo}>
        <Text style={styles.listenerText}>
          Coin Value: {coinValue ?? 'N/A'}{' '}
          {isListening ? '(listening)' : '(paused)'}
        </Text>
        <Button
          title={isListening ? 'Remove Listener' : 'Add Listener'}
          onPress={toggleListener}
        />
      </View>
      <RiveView
        style={styles.rive}
        autoPlay={true}
        dataBind={instance}
        fit={Fit.Layout}
        layoutScaleFactor={1}
        file={file}
      />
    </View>
  );
}

WithRiveFile.metadata = {
  name: 'Data Binding',
  description:
    'Shows data binding with view models, including number, string, color properties and triggers',
} satisfies Metadata;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  riveContainer: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  flex: {
    flex: 1,
  },
  rive: {
    flex: 1,
    width: '100%',
    height: '100%',
  },
  errorText: {
    color: 'red',
    textAlign: 'center',
    padding: 20,
  },
  listenerDemo: {
    padding: 16,
    backgroundColor: '#e8e8e8',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  listenerText: {
    fontSize: 16,
    fontWeight: '500',
  },
});

@mfazekas mfazekas force-pushed the feat/addListener-returns-function branch 5 times, most recently from 5807def to 55465d3 Compare December 8, 2025 16:35
@mfazekas mfazekas marked this pull request as ready for review December 8, 2025 16:37
@mfazekas mfazekas force-pushed the feat/addListener-returns-function branch from 55465d3 to d18a73b Compare December 8, 2025 16:57
@mfazekas mfazekas requested a review from HayesGordon December 8, 2025 17:05
@mfazekas mfazekas force-pushed the feat/addListener-returns-function branch from d18a73b to 71317ba Compare December 8, 2025 17:20
Comment thread example/src/pages/RiveDataBindingExample.tsx Outdated
HayesGordon
HayesGordon previously approved these changes Dec 11, 2025
Copy link
Copy Markdown
Contributor

@HayesGordon HayesGordon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! LGTM

@mfazekas mfazekas merged commit 1d571ca into main Dec 11, 2025
7 checks passed
@mfazekas mfazekas deleted the feat/addListener-returns-function branch December 11, 2025 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants