Managing Data Updates Wisely

How It Works Under the Hood

The Ray framework runs on the miniapp dual-thread architecture. When a React component's state changes, the underlying flow is:

Component calls setState / useState setter
    ↓
React executes reconciliation (diff computation)
    ↓
Ray sends changed data to the view layer via setData
    ↓
View layer updates the DOM and re-renders

Every React state update ultimately triggers an underlying setData call. Optimizing how state is updated means optimizing setData usage.

Optimization Principles

Data unrelated to rendering should not be placed in state or useState. Doing so triggers unnecessary component re-renders and underlying setData calls.

// ❌ Avoid: non-render data in state
import React, { useState } from 'react';

export default function Home() {
  const [list, setList] = useState([]);
  const [timer, setTimer] = useState(null);       // non-render data
  const [cache, setCache] = useState({});          // non-render data
  const [requestId, setRequestId] = useState('');  // non-render data
}
// ✅ Recommended: use useRef for non-render data
import React, { useState, useRef } from 'react';

export default function Home() {
  const [list, setList] = useState([]);
  const timerRef = useRef(null);       // does not trigger re-render
  const cacheRef = useRef({});         // does not trigger re-render
  const requestIdRef = useRef('');     // does not trigger re-render
}

2. Reduce Unnecessary Re-renders

State changes in a React component cause the component and its children to re-render. Avoid triggering unnecessary renders.

Use React.memo to memoize child components:

// ❌ Avoid: child re-renders every time parent updates
function ListItem({ item }) {
  return <View><Text>{item.name}</Text></View>;
}
// ✅ Recommended: use memo to prevent unnecessary re-renders
const ListItem = React.memo(function ListItem({ item }) {
  return <View><Text>{item.name}</Text></View>;
});

Use useMemo to cache computed values:

// ❌ Avoid: recomputes on every render
export default function Home() {
  const [list, setList] = useState([]);
  const sortedList = list.sort((a, b) => a.name.localeCompare(b.name));

  return <View>...</View>;
}
// ✅ Recommended: cache with useMemo
import React, { useState, useMemo } from 'react';

export default function Home() {
  const [list, setList] = useState([]);
  const sortedList = useMemo(
    () => [...list].sort((a, b) => a.name.localeCompare(b.name)),
    [list] // only recompute when list changes
  );

  return <View>...</View>;
}

3. Throttle High-Frequency State Updates

High-frequency state updates (e.g., from scroll or input events) cause frequent re-renders and setData calls.

// ❌ Avoid: update state on every scroll event
export default function Home() {
  const [scrollTop, setScrollTop] = useState(0);

  const handleScroll = (e) => {
    setScrollTop(e.detail.scrollTop);
  };

  return <ScrollView onScroll={handleScroll}>...</ScrollView>;
}
// ✅ Recommended: use throttle to limit update frequency
import React, { useState, useRef, useCallback } from 'react';

export default function Home() {
  const [scrollTop, setScrollTop] = useState(0);
  const throttleRef = useRef(null);

  const handleScroll = useCallback((e) => {
    if (throttleRef.current) return;
    throttleRef.current = setTimeout(() => {
      setScrollTop(e.detail.scrollTop);
      throttleRef.current = null;
    }, 100);
  }, []);

  return <ScrollView onScroll={handleScroll}>...</ScrollView>;
}

4. Batch Multiple State Updates

React 18+ automatically batches multiple state updates, but may not batch automatically in async callbacks:

// React auto-batches synchronous updates
const handleClick = () => {
  setName('New name');    // does not render immediately
  setAge(25);             // does not render immediately
  setStatus('active');    // all three merged into one render
};
// For closely related states, consider useReducer or merging into one state object
// ❌ Multiple separate related states
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);

// ✅ Merge into one state object
const [state, setState] = useState({
  loading: false,
  data: null,
  error: null,
});

const fetchData = async () => {
  setState(prev => ({ ...prev, loading: true }));
  try {
    const res = await api.getData();
    setState({ loading: false, data: res, error: null });
  } catch (err) {
    setState({ loading: false, data: null, error: err });
  }
};

5. Stop Updates in Background Pages

Stop state updates when the page is not visible to avoid wasting resources:

import React, { useState, useEffect, useRef } from 'react';
import { usePageEvent } from '@ray-js/ray';

export default function Home() {
  const [data, setData] = useState(null);
  const isActiveRef = useRef(true);
  const timerRef = useRef(null);

  usePageEvent('onShow', () => {
    isActiveRef.current = true;
    startPolling();
  });

  usePageEvent('onHide', () => {
    isActiveRef.current = false;
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
  });

  const startPolling = () => {
    timerRef.current = setInterval(async () => {
      if (isActiveRef.current) {
        const res = await fetchLatestData();
        setData(res);
      }
    }, 3000);
  };

  useEffect(() => {
    startPolling();
    return () => {
      if (timerRef.current) {
        clearInterval(timerRef.current);
      }
    };
  }, []);

  return <View>...</View>;
}

Checklist

Check ItemSymptomOptimization
Non-render data in stateUnnecessary re-rendersUse useRef to store it
Child components re-rendering unnecessarilyWasted performanceUse React.memo
High-frequency state updatesLaggy interactionsApply throttle/debounce
Repeated computation logicSlow renderingCache with useMemo
Multiple related states scatteredMultiple rendersMerge into one state or useReducer
Background page state updatesWasted resourcesStop updates when page is hidden