Skip to main content

Redux Debugging & Testing

Debugging and testing Redux state in RemixWarp requires understanding the store structure, using development tools effectively, and implementing comprehensive test coverage.

Development Tools​

Redux DevTools​

Access Redux DevTools through browser extensions:

// Store setup with DevTools
const configureStore = (initialState) => {
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

return createStore(
rootReducer,
initialState,
composeEnhancers(applyMiddleware(thunk))
);
};

DevTools features:

  • Time Travel: Step through action history
  • State Inspection: View current and historical state
  • Action Replay: Replay actions from any point
  • State Import/Export: Save and load state snapshots

Browser Console Debugging​

Add debugging helpers to window object:

// Add to browser console for debugging
window.inspectReduxState = () => {
const state = store.getState();
console.log('Redux State:', state);
return state;
};

window.dispatchAction = (action) => {
store.dispatch(action);
console.log('Dispatched:', action);
};

// Inspect specific state slices
window.getProjectState = () => store.getState().projectState;
window.getVMState = () => store.getState().vm;
window.getGUIState = () => store.getState().gui;

State Inspection Utilities​

// Development utilities
const createStateInspector = (store) => {
return {
// Get current state
getState: () => store.getState(),

// Get specific slice
getSlice: (path) => {
const state = store.getState();
return path.split('.').reduce((obj, key) => obj?.[key], state);
},

// Find state by predicate
findInState: (predicate, obj = store.getState(), path = '') => {
const results = [];

Object.entries(obj).forEach(([key, value]) => {
const currentPath = path ? `${path}.${key}` : key;

if (predicate(value, key, currentPath)) {
results.push({ path: currentPath, value });
}

if (typeof value === 'object' && value !== null) {
results.push(...this.findInState(predicate, value, currentPath));
}
});

return results;
},

// Monitor state changes
watchState: (path, callback) => {
let lastValue = this.getSlice(path);

return store.subscribe(() => {
const currentValue = this.getSlice(path);
if (currentValue !== lastValue) {
callback(currentValue, lastValue);
lastValue = currentValue;
}
});
}
};
};

// Usage
const inspector = createStateInspector(store);
inspector.watchState('projectState.saveState', (current, previous) => {
console.log(`Save state changed: ${previous} �?${current}`);
});

Testing Strategies​

Reducer Testing​

Test reducers in isolation:

import projectStateReducer from '../reducers/project-state';

describe('projectStateReducer', () => {
const initialState = {
projectId: null,
projectTitle: '',
isLoading: false,
error: null,
saveState: 'NOT_SAVED'
};

test('should handle SET_PROJECT_ID', () => {
const action = { type: 'SET_PROJECT_ID', projectId: '123456789' };
const newState = projectStateReducer(initialState, action);

expect(newState.projectId).toBe('123456789');
expect(newState).not.toBe(initialState); // Immutability check
});

test('should handle SET_PROJECT_LOADING', () => {
const stateWithError = { ...initialState, error: 'Previous error' };
const action = { type: 'SET_PROJECT_LOADING', isLoading: true };
const newState = projectStateReducer(stateWithError, action);

expect(newState.isLoading).toBe(true);
expect(newState.error).toBe(null); // Error should be cleared
});

test('should return current state for unknown actions', () => {
const action = { type: 'UNKNOWN_ACTION' };
const newState = projectStateReducer(initialState, action);

expect(newState).toBe(initialState);
});
});

Action Creator Testing​

import { setProjectTitle, loadProject } from '../actions/project-actions';

describe('Project Actions', () => {
test('setProjectTitle should create correct action', () => {
const title = 'My Project';
const expectedAction = {
type: 'SET_PROJECT_TITLE',
title
};

expect(setProjectTitle(title)).toEqual(expectedAction);
});

test('loadProject should handle async loading', async () => {
const mockDispatch = jest.fn();
const mockGetState = jest.fn();

// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ id: '123', title: 'Test Project' })
})
);

const thunk = loadProject('123');
await thunk(mockDispatch, mockGetState);

expect(mockDispatch).toHaveBeenCalledWith({
type: 'SET_PROJECT_LOADING',
isLoading: true
});

expect(mockDispatch).toHaveBeenCalledWith({
type: 'SET_PROJECT',
project: { id: '123', title: 'Test Project' }
});
});
});

Store Integration Testing​

Test complete store behavior:

import configureStore from '../store';

describe('Redux Store Integration', () => {
let store;

beforeEach(() => {
store = configureStore();
});

test('should initialize with correct default state', () => {
const state = store.getState();

expect(state.projectState.projectId).toBe(null);
expect(state.gui.theme.theme).toBe('light');
expect(state.vm.targets).toEqual([]);
});

test('should handle action dispatch workflow', () => {
// Set project ID
store.dispatch({
type: 'SET_PROJECT_ID',
projectId: '123456789'
});

let state = store.getState();
expect(state.projectState.projectId).toBe('123456789');

// Set project title
store.dispatch({
type: 'SET_PROJECT_TITLE',
title: 'My Project'
});

state = store.getState();
expect(state.projectState.projectTitle).toBe('My Project');
});

test('should handle multiple state updates', () => {
const actions = [
{ type: 'SET_PROJECT_ID', projectId: '123' },
{ type: 'SET_PROJECT_TITLE', title: 'Test' },
{ type: 'SET_THEME', theme: 'dark' },
{ type: 'SET_STAGE_SIZE', stageSize: 'small' }
];

actions.forEach(action => store.dispatch(action));

const state = store.getState();
expect(state.projectState.projectId).toBe('123');
expect(state.projectState.projectTitle).toBe('Test');
expect(state.gui.theme.theme).toBe('dark');
expect(state.gui.stage.stageSize).toBe('small');
});
});

Selector Testing​

import { 
getProjectStatus,
getIsProjectLoaded,
getSpriteCount
} from '../selectors';

describe('Selectors', () => {
const mockState = {
projectState: {
projectId: '123',
isLoading: false,
saveState: 'NOT_SAVED'
},
vm: {
targets: [
{ id: 'stage', isStage: true },
{ id: 'sprite1', isStage: false },
{ id: 'sprite2', isStage: false }
]
}
};

test('getIsProjectLoaded should return correct value', () => {
expect(getIsProjectLoaded(mockState)).toBe(true);

const loadingState = {
...mockState,
projectState: { ...mockState.projectState, isLoading: true }
};
expect(getIsProjectLoaded(loadingState)).toBe(false);
});

test('getProjectStatus should return correct status', () => {
expect(getProjectStatus(mockState)).toBe('unsaved');

const savedState = {
...mockState,
projectState: { ...mockState.projectState, saveState: 'SAVED' }
};
expect(getProjectStatus(savedState)).toBe('saved');
});

test('getSpriteCount should count non-stage targets', () => {
expect(getSpriteCount(mockState)).toBe(2);
});
});

Error Debugging​

Error Boundary for Redux​

class ReduxErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}

static getDerivedStateFromError(error) {
return { hasError: true, error };
}

componentDidCatch(error, errorInfo) {
console.error('Redux Error:', error);
console.error('Error Info:', errorInfo);

// Log to error tracking service
if (window.errorTracker) {
window.errorTracker.captureException(error, {
context: 'Redux State',
extra: errorInfo
});
}
}

render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong with the application state</h2>
<details>
<summary>Error Details</summary>
<pre>{this.state.error?.stack}</pre>
</details>
<button onClick={() => window.location.reload()}>
Reload Application
</button>
</div>
);
}

return this.props.children;
}
}

State Validation​

// State shape validation
const validateState = (state) => {
const errors = [];

// Validate project state
if (state.projectState.projectId && typeof state.projectState.projectId !== 'string') {
errors.push('projectState.projectId must be string or null');
}

// Validate VM state
if (!Array.isArray(state.vm.targets)) {
errors.push('vm.targets must be an array');
}

// Validate GUI state
const validThemes = ['light', 'dark', 'high-contrast'];
if (!validThemes.includes(state.gui.theme.theme)) {
errors.push(`gui.theme.theme must be one of: ${validThemes.join(', ')}`);
}

return errors;
};

// Use in development
if (process.env.NODE_ENV === 'development') {
store.subscribe(() => {
const errors = validateState(store.getState());
if (errors.length > 0) {
console.error('State validation errors:', errors);
}
});
}

Performance Debugging​

Action Performance Monitoring​

const performanceDebugger = (store) => {
const actionTimes = new Map();
const slowActions = [];

return {
startTimer: (actionType) => {
actionTimes.set(actionType, performance.now());
},

endTimer: (actionType) => {
const startTime = actionTimes.get(actionType);
if (startTime) {
const duration = performance.now() - startTime;
actionTimes.delete(actionType);

if (duration > 10) {
slowActions.push({ action: actionType, duration });
console.warn(`Slow action: ${actionType} took ${duration.toFixed(2)}ms`);
}

return duration;
}
},

getSlowActions: () => slowActions,

reset: () => {
actionTimes.clear();
slowActions.length = 0;
}
};
};

Memory Usage Tracking​

const memoryTracker = (store) => {
const measurements = [];

return {
measure: (label) => {
const state = store.getState();
const stateSize = JSON.stringify(state).length;

measurements.push({
label,
timestamp: Date.now(),
stateSize,
heapUsed: performance.memory?.usedJSHeapSize || 0
});
},

getReport: () => {
return measurements.map((m, i) => {
const prev = measurements[i - 1];
return {
...m,
stateDelta: prev ? m.stateSize - prev.stateSize : 0,
heapDelta: prev ? m.heapUsed - prev.heapUsed : 0
};
});
}
};
};

Debugging Common Issues​

State Not Updating​

// Check if reducer is handling action
const debugReducer = (reducer) => (state, action) => {
console.log(`Reducer called with action: ${action.type}`);
console.log('Previous state:', state);

const newState = reducer(state, action);

console.log('New state:', newState);
console.log('State changed:', newState !== state);

return newState;
};

Action Not Dispatching​

// Wrap dispatch to debug
const debugDispatch = (originalDispatch) => (action) => {
console.log('Dispatching action:', action);

try {
const result = originalDispatch(action);
console.log('Action dispatched successfully');
return result;
} catch (error) {
console.error('Error dispatching action:', error);
throw error;
}
};

store.dispatch = debugDispatch(store.dispatch);

Component Not Re-rendering​

// Debug useSelector
const useDebugSelector = (selector, equalityFn) => {
const selected = useSelector(selector, equalityFn);

useEffect(() => {
console.log('Selector result changed:', selected);
}, [selected]);

return selected;
};

Redux debugging in RemixWarp requires systematic approaches to testing, monitoring, and error handling to ensure reliable state management across the application.