react-native-debug-toolkit
Version:
A simple yet powerful debugging toolkit for React Native with a convenient floating UI for development
279 lines (255 loc) • 7.53 kB
JavaScript
import React, { useState, useMemo } from 'react'
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
} from 'react-native'
import ZustandLogDetails from './ZustandLogDetails'
import { getZustandActionColor } from '../utils/DebugConst'
const SubViewZustandLogs = ({ logs = [] }) => {
const [selectedLog, setSelectedLog] = useState(null)
// Memoize the sorted logs
const sortedLogs = useMemo(() => {
// Create a stable copy before sorting
return [...logs].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
}, [logs]);
// Helper to format preview of state changes
const formatActionPreview = (log) => {
if (!log) return '';
const { action = 'unknown', storeName } = log;
return `Action: ${action}${storeName ? ` (${storeName})` : ''}`;
};
// Safely stringify value for display
const safeStringify = (value) => {
if (value === undefined) return 'undefined';
if (value === null) return 'null';
const type = typeof value;
if (type === 'string') return `"${value.length > 10 ? value.substring(0, 10) + '...' : value}"`;
if (type === 'number' || type === 'boolean') return String(value);
if (Array.isArray(value)) return '[...]';
if (type === 'object') return '{...}';
return String(value);
};
// Helper to find key changes between states with more details
const findChangedKeys = (prevState, nextState) => {
if (!prevState || !nextState) return [];
const changes = [];
const allKeys = new Set([...Object.keys(prevState), ...Object.keys(nextState)]);
allKeys.forEach(key => {
const prevValue = prevState[key];
const nextValue = nextState[key];
// Check if key is added, removed, or value changed
if (!(key in prevState)) {
changes.push({
key,
type: 'added',
display: `+${key}: ${safeStringify(nextValue)}`
});
} else if (!(key in nextState)) {
changes.push({
key,
type: 'removed',
display: `-${key}: ${safeStringify(prevValue)}`
});
} else if (JSON.stringify(prevValue) !== JSON.stringify(nextValue)) {
// For primitive types, show the before and after values
const isPrimitive =
typeof prevValue !== 'object' ||
prevValue === null ||
typeof nextValue !== 'object' ||
nextValue === null;
if (isPrimitive) {
changes.push({
key,
type: 'changed',
display: `~${key}: ${safeStringify(prevValue)} → ${safeStringify(nextValue)}`
});
} else {
changes.push({
key,
type: 'changed',
display: `~${key}`
});
}
}
});
return changes;
};
const renderLogItem = ({ item }) => {
const actionPreview = formatActionPreview(item);
const actionColor = getZustandActionColor(item.action);
const timestamp = item.timestamp
? new Date(item.timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
: '';
// Get changed keys with details
const changes = findChangedKeys(item.prevState, item.nextState);
// Create a formatted display of changes
const changesText = changes.length
? changes.map(change => change.display).join(', ')
: 'No changes detected';
return (
<TouchableOpacity
style={styles.logItem}
onPress={() => setSelectedLog(item)}>
<View style={styles.logItemContainer}>
<View
style={[styles.actionIndicator, { backgroundColor: actionColor }]}
/>
<View style={styles.logContent}>
<View style={styles.logHeader}>
<Text style={[styles.actionText, { color: actionColor }]}>
{actionPreview}
</Text>
<Text style={styles.time}>{timestamp}</Text>
</View>
<Text style={styles.statePathsText} numberOfLines={2}>
{changesText}
</Text>
</View>
</View>
</TouchableOpacity>
)
}
// If a log is selected, show the details view
if (selectedLog) {
const timestamp = selectedLog.timestamp
? new Date(selectedLog.timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
: '';
const actionColor = getZustandActionColor(selectedLog.action);
const actionDisplay = selectedLog.storeName
? `${selectedLog.action} (${selectedLog.storeName})`
: selectedLog.action || 'Unknown Action';
return (
<View style={styles.container}>
<View style={styles.detailsHeader}>
<TouchableOpacity
style={styles.backButton}
onPress={() => setSelectedLog(null)}>
<Text style={styles.backButtonText}>← Back</Text>
</TouchableOpacity>
<View style={styles.headerActionTime}>
<Text style={[styles.headerAction, { color: actionColor }]}>
{actionDisplay}
</Text>
<Text style={styles.headerTimestamp}>{timestamp}</Text>
</View>
</View>
<ZustandLogDetails log={selectedLog} />
</View>
)
}
// Otherwise show the list view
return (
<View style={styles.container}>
{sortedLogs.length === 0 ? (
<Text style={styles.emptyText}>No Zustand state changes logged yet</Text>
) : (
<FlatList
data={sortedLogs}
renderItem={renderLogItem}
keyExtractor={(item, index) => `${item.timestamp}-${index}`}
style={styles.list}
/>
)}
</View>
)
}
// Adapted styles from SubViewConsoleLogs
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
list: {
flex: 1,
},
emptyText: {
textAlign: 'center',
color: '#999',
marginTop: 20,
},
logItem: {
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
logItemContainer: {
flexDirection: 'row',
paddingVertical: 10,
paddingHorizontal: 12,
},
actionIndicator: {
width: 4,
borderRadius: 2,
marginRight: 10,
},
logContent: {
flex: 1,
},
logHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 5,
},
actionText: {
fontSize: 13,
fontWeight: 'bold',
},
time: {
fontSize: 12,
color: '#888',
},
statePathsText: {
fontSize: 14,
color: '#555',
fontFamily: 'monospace',
},
// Details Header Styles
detailsHeader: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
paddingHorizontal: 15,
borderBottomWidth: 1,
borderBottomColor: '#eee',
backgroundColor: '#f8f9fa',
},
backButton: {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 4,
backgroundColor: '#eee',
marginRight: 15,
},
backButtonText: {
color: '#333',
fontWeight: '500',
fontSize: 14,
},
headerActionTime: {
flexDirection: 'row',
alignItems: 'baseline',
},
headerAction: {
fontSize: 15,
fontWeight: 'bold',
marginRight: 8,
},
headerTimestamp: {
fontSize: 13,
color: '#666',
},
})
export default SubViewZustandLogs