Skip to main content

Containers & HOCs Overview

MistWarp uses container components and Higher-Order Components (HOCs) to separate concerns between presentation and business logic.

Container Pattern

Container components in MistWarp follow the pattern:

  • Connect to Redux store for state management
  • Handle side effects and API calls
  • Pass data and callbacks to presentation components
  • Manage component lifecycle

Architecture

Containers (Smart Components)
├── Data fetching and state management
├── Event handling and side effects
└── Props transformation

Presentation Components (Dumb Components)
├── UI rendering and styling
├── User interaction handling
└── Prop validation

Common Container Pattern

// Container component
const SpriteListContainer = () => {
const sprites = useSelector(state => state.targets.sprites);
const selectedSpriteId = useSelector(state => state.targets.selectedSprite);
const dispatch = useDispatch();

const handleSelectSprite = useCallback(
(spriteId) => dispatch(setEditingTarget(spriteId)),
[dispatch]
);

const handleDeleteSprite = useCallback(
(spriteId) => dispatch(deleteSprite(spriteId)),
[dispatch]
);

return (
<SpriteList
sprites={sprites}
selectedSpriteId={selectedSpriteId}
onSelectSprite={handleSelectSprite}
onDeleteSprite={handleDeleteSprite}
/>
);
};

// Presentation component
const SpriteList = ({ sprites, selectedSpriteId, onSelectSprite, onDeleteSprite }) => (
<div className="sprite-list">
{sprites.map(sprite => (
<SpriteItem
key={sprite.id}
sprite={sprite}
isSelected={sprite.id === selectedSpriteId}
onSelect={() => onSelectSprite(sprite.id)}
onDelete={() => onDeleteSprite(sprite.id)}
/>
))}
</div>
);

Key Container Components

GUI Container

The main application container that orchestrates the entire MistWarp interface.

Stage Wrapper

Manages stage state, events, and VM integration.

Blocks Container

Handles the block workspace, toolbox, and editing state.

Manage various modal dialogs and their state.

Higher-Order Components (HOCs)

HOCs provide reusable functionality across components:

VM Connection HOC

const withVM = (WrappedComponent) => {
return (props) => {
const vm = useSelector(state => state.vm.instance);

return <WrappedComponent {...props} vm={vm} />;
};
};

// Usage
const ConnectedStage = withVM(Stage);

Loading State HOC

const withLoadingState = (WrappedComponent) => {
return ({ isLoading, loadingMessage, ...props }) => {
if (isLoading) {
return <LoadingSpinner message={loadingMessage} />;
}

return <WrappedComponent {...props} />;
};
};

Error Boundary HOC

const withErrorBoundary = (WrappedComponent) => {
return class extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

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

componentDidCatch(error, errorInfo) {
console.error('Component error:', error, errorInfo);
}

render() {
if (this.state.hasError) {
return <ErrorFallback />;
}

return <WrappedComponent {...this.props} />;
}
};
};

State Connection Patterns

Basic Redux Connection

import { useSelector, useDispatch } from 'react-redux';

const MyContainer = () => {
const data = useSelector(state => state.myData);
const dispatch = useDispatch();

const handleAction = useCallback(
(payload) => dispatch(myAction(payload)),
[dispatch]
);

return <MyComponent data={data} onAction={handleAction} />;
};

Memoized Selectors

import { createSelector } from 'reselect';

const getSprites = state => state.targets.sprites;
const getSelectedSpriteId = state => state.targets.selectedSprite;

const getSelectedSprite = createSelector(
[getSprites, getSelectedSpriteId],
(sprites, selectedId) => sprites.find(sprite => sprite.id === selectedId)
);

const SpriteEditorContainer = () => {
const selectedSprite = useSelector(getSelectedSprite);
// ...
};

Performance Considerations

Avoiding Unnecessary Re-renders

// Use React.memo for presentation components
const SpriteItem = React.memo(({ sprite, isSelected, onSelect }) => (
<div
className={`sprite-item ${isSelected ? 'selected' : ''}`}
onClick={onSelect}
>
{sprite.name}
</div>
));

// Use useCallback for event handlers
const SpriteListContainer = () => {
const handleSelectSprite = useCallback(
(spriteId) => dispatch(setEditingTarget(spriteId)),
[dispatch]
);

// ...
};

Selective State Updates

// Only subscribe to relevant state slices
const MyContainer = () => {
const relevantData = useSelector(state => ({
sprites: state.targets.sprites,
selectedId: state.targets.selectedSprite
}), shallowEqual);

// ...
};

Container Testing

describe('SpriteListContainer', () => {
let store;

beforeEach(() => {
store = createMockStore({
targets: {
sprites: [mockSprite1, mockSprite2],
selectedSprite: 'sprite1'
}
});
});

it('should pass correct props to presentation component', () => {
const wrapper = mount(
<Provider store={store}>
<SpriteListContainer />
</Provider>
);

const spriteList = wrapper.find(SpriteList);
expect(spriteList.prop('sprites')).toHaveLength(2);
expect(spriteList.prop('selectedSpriteId')).toBe('sprite1');
});

it('should dispatch action when sprite selected', () => {
const wrapper = mount(
<Provider store={store}>
<SpriteListContainer />
</Provider>
);

wrapper.find(SpriteList).prop('onSelectSprite')('sprite2');

const actions = store.getActions();
expect(actions).toContainEqual({
type: 'targets/setEditingTarget',
payload: 'sprite2'
});
});
});

Best Practices

Separation of Concerns

  • Keep containers focused on data and state management
  • Keep presentation components focused on UI and user interactions
  • Avoid mixing business logic with presentation logic

Performance Optimization

  • Use memoization for expensive calculations
  • Implement proper shouldComponentUpdate logic
  • Minimize the number of state subscriptions

Error Handling

  • Wrap containers in error boundaries
  • Handle async operation failures gracefully
  • Provide fallback UI for error states

Testing Strategy

  • Test containers and presentation components separately
  • Mock external dependencies in container tests
  • Focus on state-to-props mapping in container tests

MistWarp-Specific Patterns

VM Integration

Most containers need to interact with the MistWarp VM:

const BlocksContainer = () => {
const vm = useSelector(state => state.vm.instance);

useEffect(() => {
if (vm) {
vm.on('BLOCKS_NEED_UPDATE', handleBlocksUpdate);
return () => vm.off('BLOCKS_NEED_UPDATE', handleBlocksUpdate);
}
}, [vm]);

// ...
};

Addon System Integration

Containers may need to work with the addon system:

const withAddonSupport = (WrappedComponent) => {
return (props) => {
const addons = useSelector(state => state.addons.enabled);
const addonAPI = useAddonAPI();

return (
<WrappedComponent
{...props}
addons={addons}
addonAPI={addonAPI}
/>
);
};
};