Skip to content

fix: make useRiveProperty setValue a no-op before instance is ready#155

Merged
mfazekas merged 1 commit intomainfrom
fix/use-rive-property-noop
Feb 26, 2026
Merged

fix: make useRiveProperty setValue a no-op before instance is ready#155
mfazekas merged 1 commit intomainfrom
fix/use-rive-property-noop

Conversation

@mfazekas
Copy link
Collaborator

@mfazekas mfazekas commented Feb 23, 2026

Fixes #141 (hooks level)

useRiveProperty's setValue was throwing an error when called before the ViewModelInstance loaded. This is a common React pattern — useEffect fires on mount before async data is available.

Changed to silent no-op, as setValue does changes once the instance is available, no internal queue needed — React's dependency array handles it:

const { setValue } = useRiveString('text', instance);

// setValue is in deps — React re-fires this effect when instance loads
// and setValue's identity changes (new property → new useCallback ref)
useEffect(() => {
  setValue('Hello');
}, [setValue]);
Reproducer
import { useEffect, useMemo, useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import {
  RiveView,
  Fit,
  useRiveFile,
  useRive,
  useRiveString,
} from '@rive-app/react-native';

export default function Issue141Reproducer() {
  const { riveViewRef, setHybridRef } = useRive();
  const { riveFile, isLoading } = useRiveFile(
    require('./font_fallback.riv')
  );
  const instance = useMemo(
    () => riveFile?.defaultArtboardViewModel()?.createDefaultInstance(),
    [riveFile]
  );

  const {
    value: currentText,
    setValue: setText,
    error: textError,
  } = useRiveString('text', instance);

  const [errorLog, setErrorLog] = useState<string[]>([]);

  useEffect(() => {
    if (textError) {
      setErrorLog((prev) => [
        ...prev,
        `[${new Date().toLocaleTimeString()}] ${textError.message}`,
      ]);
    }
  }, [textError]);

  // This fires on mount before the file is loaded — triggers the error
  useEffect(() => {
    setText('Hello from useEffect');
    riveViewRef?.playIfNeeded();
  }, [setText, riveViewRef]);

  return (
    <View style={styles.container}>
      <View style={styles.info}>
        <Text style={styles.title}>Issue #141 — Hooks Level</Text>
        <Text style={styles.description}>
          setText() is called in a useEffect on mount. Since useRiveFile is
          async, instance is undefined on first render → useRiveString's
          setValue fires an error.
        </Text>
        <Text>Loading: {isLoading ? 'yes' : 'no'}</Text>
        <Text>Instance: {instance ? 'ready' : 'undefined'}</Text>
        <Text>Current text: {currentText ?? '(none)'}</Text>
        <Text style={textError && styles.errorText}>
          Error: {textError ? textError.message : '(none)'}
        </Text>
      </View>

      <View style={styles.riveContainer}>
        {riveFile && instance ? (
          <RiveView
            style={styles.rive}
            autoPlay
            dataBind={instance}
            fit={Fit.Contain}
            file={riveFile}
            hybridRef={setHybridRef}
          />
        ) : (
          <Text>{isLoading ? 'Loading...' : 'No file'}</Text>
        )}
      </View>

      <View style={styles.logContainer}>
        <Text style={styles.logTitle}>Error Log</Text>
        {errorLog.length === 0 ? (
          <Text style={styles.logPlaceholder}>No errors recorded</Text>
        ) : (
          errorLog.map((entry, i) => (
            <Text key={i} style={styles.logEntry}>{entry}</Text>
          ))
        )}
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#fff' },
  info: { padding: 16, gap: 6 },
  title: { fontSize: 18, fontWeight: '700' },
  description: { fontSize: 13, color: '#666', lineHeight: 20 },
  errorText: { color: '#FF3B30' },
  riveContainer: {
    height: 200, backgroundColor: '#f0f0f0',
    justifyContent: 'center', alignItems: 'center',
  },
  rive: { width: '100%', height: '100%' },
  logContainer: {
    flex: 1, margin: 16, backgroundColor: '#1a1a2e',
    borderRadius: 10, padding: 12,
  },
  logTitle: {
    color: '#888', fontSize: 12, fontWeight: '600',
    textTransform: 'uppercase', marginBottom: 8,
  },
  logPlaceholder: { color: '#666', fontSize: 12, fontFamily: 'monospace' },
  logEntry: { color: '#FF6B6B', fontSize: 11, fontFamily: 'monospace' },
});

@mfazekas mfazekas force-pushed the fix/use-rive-property-noop branch 10 times, most recently from 7622ffc to 9bafbe1 Compare February 23, 2026 19:58
@mfazekas mfazekas requested a review from HayesGordon February 23, 2026 20:02
@mfazekas mfazekas force-pushed the fix/use-rive-property-noop branch from 9bafbe1 to 4a5af1b Compare February 24, 2026 06:18
`Cannot set value for property "${path}" because it was not found. Your view model instance may be undefined, or the path may be incorrect.`
)
);
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

What about the scenario where the property is in fact invalid?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What about the scenario where the property is in fact invalid?

There is an useEffect on line 63 covering that.

useEffect(() => {
if (viewModelInstance && !property) {
setError(
new Error(`Property "${path}" not found in the ViewModel instance`)
);
}
}, [viewModelInstance, property, path]);

And I've added a test case covering this:

it('should return error when property is not found on a valid instance', () => {
const mockInstance = createMockViewModelInstance({});
const { result } = renderHook(() =>
useRiveProperty<any, string>(mockInstance, 'nonexistent/path', {
getProperty: (vmi, path) => (vmi as any).enumProperty(path),
})
);
const [, , error] = result.current;
expect(error).toBeInstanceOf(Error);
expect(error?.message).toContain('nonexistent/path');
});

Copy link
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.

LGTM!

@mfazekas mfazekas enabled auto-merge (squash) February 26, 2026 06:19
…141)

setValue was throwing an error when called before the ViewModelInstance
loaded. This is a common React pattern — useEffect fires on mount before
async data is available. Changed to silent no-op matching react-hook-form
and React 18 conventions. React's dependency array re-fires the effect
when setValue identity changes after instance loads.

Adds Jest unit tests and harness test for the behavior.
@mfazekas mfazekas force-pushed the fix/use-rive-property-noop branch from 4a5af1b to a4177c6 Compare February 26, 2026 06:19
@mfazekas mfazekas merged commit b5fb514 into main Feb 26, 2026
8 of 9 checks passed
@mfazekas mfazekas deleted the fix/use-rive-property-noop branch February 26, 2026 06:50
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.

Commands called before Rive file/view is ready are dropped or fail (need internal queue/readiness gating)

2 participants