react-presentation-container
v1.0.1
Published
A lightweight React library for separating presentation and container logic using the higher-order component pattern
Maintainers
Readme
React Presentation Container
A lightweight React library for separating presentation and container logic using the higher-order component pattern. This helps create cleaner, more maintainable React components by separating state management and business logic from presentation concerns.
Why Use This Library?
- Clean Architecture: Separate your UI from business logic
- Better Testing: Test presentation and logic independently
- Reusable Components: Share presentation components across different contexts
- Maintainable Code: Clearer separation of concerns
Installation
npm install react-presentation-containerFeatures
- ✅ Multiple module formats: ESM, CommonJS, and UMD builds
- ✅ TypeScript support: Full type definitions included
- ✅ Tree-shakeable: Optimized for modern bundlers
- ✅ Zero dependencies: Only requires React as a peer dependency
- ✅ Framework agnostic: Works with Next.js, Vite, CRA, and more
- ✅ Small bundle size: < 2KB minified + gzipped
Usage
Best Practice: File Structure
For optimal organization, use this file structure pattern:
UserCard.component.jsx (Presentation only)
import React from 'react';
const UserCard = ({ name, email, controller }) => (
<div className="user-card">
<h2>{name}</h2>
<p>{email}</p>
<button onClick={controller.handleClick}>
Clicks: {controller.clickCount}
</button>
{controller.loading && <p>Loading...</p>}
</div>
);
export default UserCard;UserCard.container.js (Container with inline controller)
import React, { useState, useEffect } from 'react';
import PresentationContainer from 'react-presentation-container';
import UserCard from './UserCard.component';
export default PresentationContainer({
component: UserCard,
controller: (props) => {
const [clickCount, setClickCount] = useState(0);
const [loading, setLoading] = useState(false);
const handleClick = () => {
setClickCount(c => c + 1);
};
const fetchUserData = async () => {
setLoading(true);
// API call logic here
setLoading(false);
};
useEffect(() => {
fetchUserData();
}, [props.userId]);
return {
clickCount,
loading,
handleClick
};
}
});Alternative: Single File Example
import React, { useState } from 'react';
import PresentationContainer from 'react-presentation-container';
// Presentation Component (UI only)
const UserCard = ({ name, email, controller }) => (
<div>
<h2>{name}</h2>
<p>{email}</p>
<button onClick={controller.handleClick}>
Clicks: {controller.clickCount}
</button>
</div>
);
// Container with inline controller
export default PresentationContainer({
component: UserCard,
controller: (props) => {
const [clickCount, setClickCount] = useState(0);
return {
clickCount,
handleClick: () => setClickCount(c => c + 1)
};
}
});Import Methods
ES Modules (Modern Bundlers)
import PresentationContainer from 'react-presentation-container';CommonJS (Node.js/Legacy)
const PresentationContainer = require('react-presentation-container').default;UMD (Browser/CDN)
<script src="https://unpkg.com/react-presentation-container/dist/index.js"></script>
<script>
const PresentationContainer = window.ReactPresentationContainer;
</script>TypeScript
Full TypeScript support with type definitions included:
import PresentationContainer, { PresentationContainerOptions } from 'react-presentation-container';
interface Props {
name: string;
email: string;
}
interface ControllerState {
clickCount: number;
handleClick: () => void;
}
const options: PresentationContainerOptions<Props, {}, ControllerState> = {
component: UserCardPresentation,
controller: UserCardController,
bindMembers: ['handleClick']
};API Reference
PresentationContainer(options)
Creates a container component that manages the relationship between presentation and controller.
Returns: ComponentType<P> | null
Options
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| component | ComponentType<P & { controller: C }> | ✅ | The presentation component to render |
| controller | ComponentType<P> \| ((props: P, ...args: any[]) => C) | ❌ | React class component or function that manages state and business logic |
| filterProps | (props: P) => Partial<P> | ❌ | Function to filter/transform props passed to presentation component |
| bindMembers | string[] | ❌ | Array of controller instance members to bind (class components only) |
| middleware | Array<(component: ComponentType) => ComponentType> | ❌ | Array of higher-order components to wrap the container |
TypeScript Types
interface PresentationContainerOptions<P = any, S = any, C = any> {
component: ComponentType<P & { controller: C }>;
controller?: ComponentType<P> | ((props: P, ...args: any[]) => C);
filterProps?: (props: P) => Partial<P>;
bindMembers?: string[];
middleware?: Array<(component: ComponentType) => ComponentType>;
}Advanced Usage
With Middleware
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import UserCard from './UserCard.component';
export default PresentationContainer({
component: UserCard,
controller: (props) => {
// Controller logic here
return { /* controller state and methods */ };
},
middleware: [withRouter, connect(mapStateToProps)]
});Filtering Props
import UserCard from './UserCard.component';
export default PresentationContainer({
component: UserCard,
controller: (props) => {
// Controller logic here
return { /* controller state and methods */ };
},
filterProps: (props) => {
// Only pass specific props to presentation
const { name, email } = props;
return { name, email };
}
});Class Component Controller (Legacy)
import UserCard from './UserCard.component';
class UserCardController extends React.Component {
state = {
loading: false,
user: null
};
componentDidMount() {
this.fetchUser();
}
fetchUser = async () => {
this.setState({ loading: true });
const user = await api.getUser(this.props.userId);
this.setState({ user, loading: false });
};
handleRefresh = () => {
this.fetchUser();
};
}
export default PresentationContainer({
component: UserCard,
controller: UserCardController,
bindMembers: ['handleRefresh'] // Bind methods to controller instance
});Framework Compatibility
This library is designed to work with all modern JavaScript frameworks and build tools:
- ✅ Next.js (App Router & Pages Router)
- ✅ Vite
- ✅ Create React App
- ✅ Gatsby
- ✅ Remix
- ✅ Parcel
- ✅ Webpack 4/5
- ✅ Rollup
- ✅ esbuild
Requirements
- React: 17.0.0 or higher
- Node.js: 14 or higher (for development)
- Browsers: Modern browsers (Chrome, Firefox, Safari, Edge)
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT © Nick Pray
