hadyfayed / filament-react-wrapper
React component integration system for FilamentPHP applications
Installs: 1
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Language:TypeScript
Requires
- php: ^8.2
- filament/filament: ^3.0
- laravel/framework: ^11.0|^12.0
- livewire/livewire: ^3.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0
README
A comprehensive React integration system for Laravel and Filament applications, providing seamless component registration, state management, and real-time synchronization with built-in memory leak prevention and infinite loop protection.
🚀 Features
- 🔧 Universal Component System - Register and render React components anywhere in your Laravel app
- 🎯 Advanced State Management - Built-in state management with persistence and Livewire sync
- ⚡ Performance Optimized - Lazy loading, memoization, efficient re-rendering, and memory leak prevention
- 🛡️ Type Safe - Full TypeScript support with comprehensive type definitions
- 🔄 Real-time Sync - Bidirectional data flow with Livewire components
- 📦 Zero Config - Works out of the box with sensible defaults
- 🎨 Filament Ready - Native integration with Filament admin panels
- 🧩 Extensible - Plugin system with middleware support
- 🔒 Security First - XSS protection, input validation, and secure prop handling
- 🚫 Loop Protection - Built-in infinite loop detection and prevention
- 🧠 Memory Safe - Automatic cleanup and bounded data structures
📦 Installation
NPM Package
npm install @hadyfayed/react-wrapper
Composer Package
composer require hadyfayed/react-wrapper
Laravel Setup
- Publish the service provider:
php artisan vendor:publish --provider="HadyFayed\ReactWrapper\ReactWrapperServiceProvider"
- Register the Filament plugin:
// app/Providers/Filament/AdminPanelProvider.php use HadyFayed\ReactWrapper\FilamentReactWrapperPlugin; public function panel(Panel $panel): Panel { return $panel ->plugins([ FilamentReactWrapperPlugin::make(), ]); }
- Add to your
app.js
:
import '@hadyfayed/react-wrapper';
- Build your assets:
npm run build
🎯 Quick Start
1. Register a React Component
import { componentRegistry } from '@hadyfayed/react-wrapper'; import MyComponent from './components/MyComponent'; // Simple registration componentRegistry.register({ name: 'MyComponent', component: MyComponent, defaultProps: { message: 'Hello World!' } });
2. Use in Blade Templates
<!-- Basic usage --> <div data-react-component="MyComponent"></div> <!-- With props --> <div data-react-component="MyComponent" data-react-props='{"title": "Custom Title", "count": 5}' ></div> <!-- With state synchronization --> <div data-react-component="MyComponent" data-react-state-path="user.preferences" data-react-props='{"userId": {{ $user->id }}}' ></div>
3. Use in Filament
use HadyFayed\ReactWrapper\Components\ReactComponent; class EditUser extends EditRecord { protected function getHeaderActions(): array { return [ ReactComponent::make('UserProfileEditor') ->props([ 'userId' => $this->record->id, 'editable' => true ]) ->statePath('userProfile') ]; } }
📚 Component Registration
Basic Registration
import { componentRegistry } from '@hadyfayed/react-wrapper'; componentRegistry.register({ name: 'UserCard', component: UserCard, defaultProps: { showAvatar: true, size: 'medium' }, metadata: { category: 'user', description: 'Displays user information in a card format', tags: ['user', 'display', 'card'] } });
Advanced Registration with Lazy Loading
componentRegistry.register({ name: 'HeavyComponent', component: () => import('./components/HeavyComponent'), isAsync: true, config: { lazy: true, cache: true, preload: false }, metadata: { category: 'charts', description: 'Advanced data visualization component' } });
Bulk Registration
import { registerComponents } from '@hadyfayed/react-wrapper'; const components = [ { name: 'Button', component: Button }, { name: 'Modal', component: Modal }, { name: 'Form', component: Form } ]; registerComponents(components);
Registration from Laravel/PHP
You can also register React components from the Laravel/PHP side, which is useful for package authors or when you want to manage component registration through Laravel configuration:
Service Provider Registration
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use HadyFayed\ReactWrapper\Services\ReactComponentRegistry; class ReactComponentServiceProvider extends ServiceProvider { public function boot() { $registry = app(ReactComponentRegistry::class); // Register components with PHP $registry->register([ 'name' => 'UserCard', 'component_path' => 'resources/js/components/UserCard.tsx', 'default_props' => [ 'showAvatar' => true, 'size' => 'medium' ], 'metadata' => [ 'category' => 'user', 'description' => 'Displays user information in a card format', 'tags' => ['user', 'display', 'card'] ] ]); // Register multiple components $registry->registerMany([ [ 'name' => 'ProductCard', 'component_path' => 'resources/js/components/ProductCard.tsx', 'default_props' => ['showPrice' => true], 'metadata' => ['category' => 'product'] ], [ 'name' => 'OrderSummary', 'component_path' => 'resources/js/components/OrderSummary.tsx', 'is_async' => true, 'config' => ['lazy' => true, 'cache' => true] ] ]); } }
Configuration-Based Registration
Add to config/react-wrapper.php
:
<?php return [ 'components' => [ 'auto_register' => true, 'scan_directories' => [ 'resources/js/components', 'resources/js/widgets' ], 'registered_components' => [ 'UserProfile' => [ 'component_path' => 'resources/js/components/UserProfile.tsx', 'default_props' => ['editable' => false], 'config' => ['lazy' => false, 'cache' => true], 'metadata' => [ 'category' => 'user', 'description' => 'User profile management component' ] ], 'DataTable' => [ 'component_path' => 'resources/js/components/DataTable.tsx', 'is_async' => true, 'default_props' => [ 'pageSize' => 10, 'sortable' => true ], 'config' => [ 'lazy' => true, 'cache' => true, 'preload' => false ] ] ] ] ];
Artisan Command for Registration
Generate component registration via Artisan:
# Create a new React component with auto-registration php artisan make:react-component UserDashboard --register # Register an existing component php artisan react:register ProductCard --path=resources/js/components/ProductCard.tsx # Register multiple components from a directory php artisan react:scan resources/js/components --register
Facade Usage
Use the ReactWrapper facade for quick registration:
<?php use HadyFayed\ReactWrapper\Facades\ReactWrapper; // In a controller, middleware, or service class ComponentController extends Controller { public function registerComponent() { ReactWrapper::register([ 'name' => 'DynamicChart', 'component_path' => 'resources/js/charts/DynamicChart.tsx', 'default_props' => [ 'type' => 'line', 'animated' => true ], 'config' => [ 'lazy' => true, 'cache' => false // Don't cache dynamic components ] ]); return response()->json(['message' => 'Component registered successfully']); } public function getRegisteredComponents() { $components = ReactWrapper::getRegistered(); return response()->json($components); } }
Package Integration
For Laravel package authors:
<?php namespace YourPackage\Providers; use Illuminate\Support\ServiceProvider; use HadyFayed\ReactWrapper\Services\ReactComponentRegistry; class YourPackageServiceProvider extends ServiceProvider { public function boot() { // Register package components automatically $this->registerPackageComponents(); // Publish package components $this->publishes([ __DIR__.'/../resources/js/components' => resource_path('js/vendor/your-package'), ], 'your-package-components'); } protected function registerPackageComponents() { $registry = app(ReactComponentRegistry::class); $packageComponents = [ 'PackageWidget' => [ 'component_path' => 'js/vendor/your-package/PackageWidget.tsx', 'default_props' => ['theme' => 'package-default'], 'metadata' => [ 'category' => 'package-widgets', 'package' => 'your-package', 'version' => '1.0.0' ] ] ]; foreach ($packageComponents as $name => $config) { $registry->register(array_merge($config, ['name' => $name])); } } }
Dynamic Registration in Filament
Register components dynamically in Filament resources:
<?php namespace App\Filament\Resources; use Filament\Resources\Resource; use HadyFayed\ReactWrapper\Services\ReactComponentRegistry; class UserResource extends Resource { public static function boot() { parent::boot(); // Register resource-specific components app(ReactComponentRegistry::class)->register([ 'name' => 'UserResourceChart', 'component_path' => 'resources/js/filament/UserResourceChart.tsx', 'default_props' => [ 'resource' => 'users', 'metric' => 'registrations' ] ]); } public static function form(Form $form): Form { return $form->schema([ ReactField::make('user_analytics') ->component('UserResourceChart') ->props(['userId' => fn($record) => $record?->id]) ->visible(fn($record) => $record !== null) ]); } }
🎯 State Management
Using State Manager Provider
import { StateManagerProvider, useStateManager } from '@hadyfayed/react-wrapper'; function App() { return ( <StateManagerProvider initialState={{ user: { name: 'John' } }} onStateChange={(state) => console.log('State changed:', state)} syncPath="app.state" > <MyComponent /> </StateManagerProvider> ); } function MyComponent() { const { state, setState, getState } = useStateManager(); const updateUser = () => { setState('user.name', 'Jane Doe'); }; return ( <div> <p>User: {getState('user.name')}</p> <button onClick={updateUser}>Update Name</button> </div> ); }
Using State Path Hook
import { useStatePath } from '@hadyfayed/react-wrapper'; function UserProfile() { const [user, setUser] = useStatePath('user', { name: '', email: '' }); return ( <form> <input value={user.name} onChange={(e) => setUser(prev => ({ ...prev, name: e.target.value }))} /> <input value={user.email} onChange={(e) => setUser(prev => ({ ...prev, email: e.target.value }))} /> </form> ); }
Global State Manager
import { globalStateManager } from '@hadyfayed/react-wrapper'; // Set global state globalStateManager.setState('app.theme', 'dark'); // Get global state const theme = globalStateManager.getState('app.theme'); // Subscribe to changes const unsubscribe = globalStateManager.subscribe('app.theme', (newTheme) => { console.log('Theme changed to:', newTheme); }); // Clean up unsubscribe();
💾 State Persistence
Using Persisted State Hook
import { usePersistedState } from '@hadyfayed/react-wrapper'; function ThemeSelector() { const [theme, setTheme] = usePersistedState('theme', 'light', { storage: 'localStorage', syncWithLivewire: true, livewirePath: 'user.theme' }); return ( <select value={theme} onChange={(e) => setTheme(e.target.value)}> <option value="light">Light</option> <option value="dark">Dark</option> </select> ); }
Manual Persistence Service
import { statePersistenceService } from '@hadyfayed/react-wrapper'; // Register a persistence config statePersistenceService.register({ key: 'userPreferences', storage: 'localStorage', syncWithLivewire: true, livewirePath: 'user.preferences', debounceMs: 500, transformer: { serialize: (data) => JSON.stringify(data), deserialize: (data) => JSON.parse(data) } }); // Save data statePersistenceService.save('userPreferences', { theme: 'dark', language: 'en' }); // Load data const preferences = statePersistenceService.load('userPreferences');
🎨 Filament Integration
Creating Filament Components
<?php namespace App\Filament\Components; use HadyFayed\ReactWrapper\Components\ReactComponent; class UserDashboard extends ReactComponent { protected string $component = 'UserDashboard'; public static function make(string $component = null): static { return new static($component ?? static::$component); } public function userId(int $userId): static { $this->props(['userId' => $userId]); return $this; } public function editable(bool $editable = true): static { $this->props(['editable' => $editable]); return $this; } }
Using in Filament Pages
use App\Filament\Components\UserDashboard; class Dashboard extends Page { protected static string $view = 'filament.pages.dashboard'; protected function getHeaderWidgets(): array { return [ UserDashboard::make() ->userId(auth()->id()) ->editable(true) ->statePath('dashboard.user') ]; } }
Form Field Integration
use HadyFayed\ReactWrapper\Forms\Components\ReactField; class UserForm extends Form { public function form(Form $form): Form { return $form->schema([ ReactField::make('profile_editor') ->component('UserProfileEditor') ->props([ 'allowImageUpload' => true, 'maxImageSize' => '2MB' ]) ->statePath('profile') ->reactive() ->afterStateUpdated(function ($state) { // Handle state updates $this->user->update($state); }) ]); } }
🔧 Advanced Features
Middleware System
import { componentRegistry } from '@hadyfayed/react-wrapper'; // Global middleware componentRegistry.addMiddleware((component, props, context) => { // Add analytics tracking return (componentProps) => { React.useEffect(() => { analytics.track('component_rendered', { component: context.metadata.name, props: Object.keys(componentProps) }); }, []); return React.createElement(component, componentProps); }; }); // Component-specific middleware componentRegistry.register({ name: 'SecureComponent', component: SecureComponent, config: { middleware: [ (component, props, context) => { // Add authentication check return (componentProps) => { if (!componentProps.user?.authenticated) { return React.createElement('div', null, 'Access Denied'); } return React.createElement(component, componentProps); }; } ] } });
Error Handling
All components are automatically wrapped with error boundaries that provide:
- Graceful error display
- Error reporting to console
- Retry functionality
- Custom error handlers
componentRegistry.register({ name: 'RiskyComponent', component: RiskyComponent, config: { onError: (error, componentName) => { // Custom error handling console.error(`Error in ${componentName}:`, error); // Report to error tracking service errorTracker.report(error, { component: componentName }); } } });
🚀 Performance & Memory Safety
Memory Leak Prevention
The system includes comprehensive protection against memory leaks:
// Automatic cleanup on component unmount React.useEffect(() => { const unsubscribe = globalStateManager.subscribe('user.data', handleUserChange); return () => { unsubscribe(); // Automatic cleanup }; }, []); // Bounded data structures prevent unlimited growth // Maximum 1000 entries in persistence service with automatic cleanup
Infinite Loop Protection
Built-in protection against infinite loops in state updates:
// Circular notification detection if (this._notifyingPaths.has(path)) { console.warn(`Circular notification detected for path: ${path}`); return; // Prevents infinite loops }
Performance Optimizations
- Memoization - Automatic memoization of components and state
- Debouncing - Built-in debouncing for state changes and notifications
- Lazy Loading - Code splitting for large components
- Efficient Re-rendering - Smart prop comparison and update batching
🔒 Security Features
Input Validation
// Validate props before rendering componentRegistry.register({ name: 'SecureComponent', component: SecureComponent, config: { middleware: [ (component, props, context) => { return (componentProps) => { // Validate props const validatedProps = validateProps(componentProps, { userId: 'number', email: 'email', role: ['admin', 'user', 'guest'] }); return React.createElement(component, validatedProps); }; } ] } });
XSS Prevention
// Sanitize HTML content import DOMPurify from 'dompurify'; const SafeComponent = ({ htmlContent }) => { const sanitizedHTML = DOMPurify.sanitize(htmlContent); return <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />; };
🛠️ API Reference
Component Registry
interface IComponentRegistry { register(definition: IComponentDefinition): void; get(name: string): IComponentDefinition | undefined; create(name: string, props?: Record<string, any>): React.ComponentType<any> | null; has(name: string): boolean; unregister(name: string): boolean; clear(): void; getComponentNames(): string[]; getStats(): ComponentStats; mount(componentName: string, containerId: string, props?: Record<string, any>): void; unmount(containerId: string): void; on(event: string, callback: Function, priority?: number): void; off(event: string, callback: Function): void; }
State Manager
interface IStateManager { setState(path: string, value: any): void; updateState(path: string, updater: (current: any) => any): void; getState(path: string): any; resetState(newState?: StateManagerState): void; batchUpdate(updates: Array<{ path: string; value: any }>): void; subscribe(path: string, callback: (value: any) => void): () => void; }
Hooks
// State Manager Hook const useStateManager = (): StateManagerContextType => { ... }; // State Path Hook const useStatePath = <T = any>( path: string, defaultValue?: T ): [T, (value: T | ((prev: T) => T)) => void] => { ... }; // Persisted State Hook const usePersistedState = <T>( key: string, defaultValue: T, config?: Partial<StatePersistenceConfig> ): [T, (value: T | ((prev: T) => T)) => void] => { ... };
PHP/Laravel API
ReactComponentRegistry Service
<?php namespace HadyFayed\ReactWrapper\Services; interface ReactComponentRegistryInterface { public function register(array $definition): void; public function registerMany(array $definitions): void; public function get(string $name): ?array; public function has(string $name): bool; public function unregister(string $name): bool; public function clear(): void; public function getRegistered(): array; public function getStats(): array; public function scan(string $directory): array; }
ReactWrapper Facade
<?php use HadyFayed\ReactWrapper\Facades\ReactWrapper; // Register component ReactWrapper::register([ 'name' => 'ComponentName', 'component_path' => 'path/to/component.tsx', 'default_props' => ['prop' => 'value'], 'config' => ['lazy' => true, 'cache' => true], 'metadata' => ['category' => 'widgets'] ]); // Register multiple components ReactWrapper::registerMany($components); // Get component definition $definition = ReactWrapper::get('ComponentName'); // Check if component exists $exists = ReactWrapper::has('ComponentName'); // Get all registered components $components = ReactWrapper::getRegistered(); // Get registration statistics $stats = ReactWrapper::getStats(); // Scan directory for components $found = ReactWrapper::scan('resources/js/components');
ReactField Form Component
<?php use HadyFayed\ReactWrapper\Forms\Components\ReactField; ReactField::make('field_name') ->component('ComponentName') // React component name ->props(['key' => 'value']) // Component props ->defaultProps(['default' => true]) // Default props ->statePath('form.field') // State synchronization path ->height(400) // Container height ->width('100%') // Container width ->reactive() // Make field reactive ->live() // Enable live updates ->debounce(300) // Debounce updates (ms) ->afterStateUpdated(fn($state) => ...) // State change callback ->visible(fn($get) => $get('show')) // Conditional visibility ->disabled(fn($get) => !$get('edit')) // Conditional disable ->required() // Mark as required ->columnSpan(2) // Grid column span ->extraAttributes(['class' => 'custom']); // Additional HTML attributes
ReactWidget
<?php use HadyFayed\ReactWrapper\Widgets\ReactWidget; class CustomWidget extends ReactWidget { protected string $component = 'WidgetComponent'; protected function getProps(): array { return [ 'title' => 'Widget Title', 'data' => $this->getData(), 'config' => $this->getConfig() ]; } protected function getHeight(): ?string { return '300px'; } protected function getStatePath(): ?string { return 'widgets.custom'; } public static function canView(): bool { return auth()->user()?->can('view-widgets') ?? false; } }
Configuration Options
<?php // config/react-wrapper.php return [ 'debug' => env('REACT_WRAPPER_DEBUG', false), 'cache_components' => env('REACT_WRAPPER_CACHE', true), 'preload_components' => env('REACT_WRAPPER_PRELOAD', false), 'max_state_size' => env('REACT_WRAPPER_MAX_STATE_SIZE', 1000), 'components' => [ 'auto_register' => true, 'scan_directories' => [ 'resources/js/components', 'resources/js/widgets' ], 'registered_components' => [ // Component definitions ] ], 'memory_safety' => [ 'max_subscriptions_per_path' => 100, 'cleanup_interval' => 300000, // 5 minutes 'enable_loop_detection' => true, ], 'security' => [ 'validate_props' => true, 'sanitize_html' => true, 'max_prop_size' => 1024 * 1024, // 1MB ] ];
Artisan Commands
# Create React component with registration php artisan make:react-component ComponentName --register # Register existing component php artisan react:register ComponentName --path=resources/js/components/ComponentName.tsx # Scan and register components from directory php artisan react:scan resources/js/components --register # List registered components php artisan react:list # Clear component registry php artisan react:clear # Show component statistics php artisan react:stats # Generate component registration file php artisan react:export --output=bootstrap-components.php
Event Hooks
<?php use HadyFayed\ReactWrapper\Events\ComponentRegistered; use HadyFayed\ReactWrapper\Events\ComponentRendered; // Listen for component registration Event::listen(ComponentRegistered::class, function ($event) { Log::info('Component registered: ' . $event->componentName); }); // Listen for component rendering Event::listen(ComponentRendered::class, function ($event) { // Track component usage Analytics::track('component_rendered', [ 'component' => $event->componentName, 'props' => $event->props ]); });
🐛 Debugging & Troubleshooting
Enable Debug Mode
// Enable debug logging window.ReactWrapperConfig = { debug: true, logLevel: 'verbose' };
Common Issues
Component Not Found
// Check if component is registered if (!componentRegistry.has('MyComponent')) { console.log('Available components:', componentRegistry.getComponentNames()); } // Check component statistics console.log('Registry stats:', componentRegistry.getStats());
State Not Syncing
// Check Livewire connection if (!window.workflowDataSync) { console.warn('Livewire sync not available'); } // Monitor state changes globalStateManager.subscribe('', (state) => { console.log('Global state changed:', state); });
Memory Leaks
// Monitor subscription counts console.log('Active subscriptions:', globalStateManager.subscribers.size); // Monitor active components console.log('Active components:', universalReactRenderer.getActiveContainers());
📊 Testing
Unit Testing Components
import { render, screen } from '@testing-library/react'; import { componentRegistry } from '@hadyfayed/react-wrapper'; import MyComponent from './MyComponent'; describe('Component Registry', () => { beforeEach(() => { componentRegistry.clear(); }); test('registers and creates component', () => { componentRegistry.register({ name: 'TestComponent', component: MyComponent }); expect(componentRegistry.has('TestComponent')).toBe(true); const Component = componentRegistry.create('TestComponent', { title: 'Test' }); expect(Component).toBeDefined(); }); });
Integration Testing
import { StateManagerProvider, useStatePath } from '@hadyfayed/react-wrapper'; const TestComponent = () => { const [value, setValue] = useStatePath('test.value', 'initial'); return ( <div> <span data-testid="value">{value}</span> <button onClick={() => setValue('updated')}>Update</button> </div> ); }; test('state management works', () => { render( <StateManagerProvider> <TestComponent /> </StateManagerProvider> ); expect(screen.getByTestId('value')).toHaveTextContent('initial'); fireEvent.click(screen.getByText('Update')); expect(screen.getByTestId('value')).toHaveTextContent('updated'); });
🚀 Deployment
Production Build
# Build for production npm run build # Laravel asset compilation php artisan filament:assets
Environment Configuration
// config/react-wrapper.php return [ 'debug' => env('REACT_WRAPPER_DEBUG', false), 'cache_components' => env('REACT_WRAPPER_CACHE', true), 'preload_components' => env('REACT_WRAPPER_PRELOAD', false), 'max_state_size' => env('REACT_WRAPPER_MAX_STATE_SIZE', 1000), 'memory_safety' => [ 'max_subscriptions_per_path' => 100, 'cleanup_interval' => 300000, // 5 minutes 'enable_loop_detection' => true, ], ];
🤝 Contributing
We welcome contributions! Please see our Contributing Guide for details.
Development Setup
# Clone the repository git clone https://github.com/hadyfayed/react-wrapper.git # Install dependencies npm install composer install # Run tests npm test php artisan test # Start development server npm run dev php artisan serve
Code Style
We use ESLint and Prettier for code formatting:
npm run lint npm run format
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
🙏 Acknowledgments
- Built with ❤️ for the Laravel and React communities
- Inspired by modern component architecture patterns
- Special thanks to all contributors and maintainers
📞 Support
Made with ❤️ by Hady Fayed