Modern React/GraphQL Components
Modern front-end architecture with React, Redux, and build tools.
Overviewβ
The APS platform is progressively integrating a modern front-end architecture based on React and modern build tools (Webpack), while maintaining compatibility with legacy code based on jQuery and Knockout.
This hybrid approach allows for a progressive migration to modern technologies without a complete rewrite of the application.
Technical Stackβ
Front-end Technologiesβ
Core Libraries:
- React 17: Declarative UI library
- React DOM 17: React rendering in the DOM
- React Redux 7.2: State management with Redux
- Redux Saga 1.1: Asynchronous side effect management
- Redux Toolkit 1.7: Modern Redux tools
Styling Libraries:
- styled-components 5.3: CSS-in-JS
- polished 4.1: Style utilities
- classnames 2.3: Conditional CSS class management
Utilities:
- prop-types 15.8: React props validation
- use-debounce 7.0: Debounce hook
- react-content-loader 6.2: Loading skeletons
Legacy Technologiesβ
The application still uses:
- jQuery 3.6: DOM manipulation and AJAX
- Knockout 3.5: MVVM data-binding
- jQuery UI 1.12: UI components
- SignalR 2.4: Real-time communication
- DevExpress: Grid controls and advanced components
Build Toolsβ
Webpack 5:
- JavaScript/React module bundling
- Minification and optimization
- Source maps for debugging
Babel 7:
- ES6+ β ES5 transpilation
- JSX support (React)
- Polyfills for browser compatibility
Gulp 4:
- SASS β CSS compilation
- File concatenation
- Watch mode for development
TypeScript 4.7:
- Typed build scripts
- Administration tools
React Architectureβ
Component Organizationβ
React components are organized in:
Web/Model/Scripts/form/
βββ src/
β βββ components/ # Reusable React components
β βββ containers/ # Redux-connected components
β βββ store/ # Redux configuration
β βββ sagas/ # Redux Sagas
β βββ utils/ # Utilities
βββ webpack.config.js # Webpack configuration
Redux Patternβ
Application state is managed with Redux via Redux Toolkit:
Store:
import { configureStore } from '@reduxjs/toolkit';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(sagaMiddleware),
});
sagaMiddleware.run(rootSaga);
Redux Toolkit Slices:
import { createSlice } from '@reduxjs/toolkit';
const documentSlice = createSlice({
name: 'document',
initialState: {
currentDocument: null,
loading: false,
error: null,
},
reducers: {
loadDocument: (state, action) => {
state.loading = true;
},
loadDocumentSuccess: (state, action) => {
state.currentDocument = action.payload;
state.loading = false;
},
loadDocumentFailure: (state, action) => {
state.error = action.payload;
state.loading = false;
},
},
});
export const { loadDocument, loadDocumentSuccess, loadDocumentFailure }
= documentSlice.actions;
export default documentSlice.reducer;
Redux Saga for Asynchronous Effectsβ
Asynchronous operations (API calls) are handled via Redux Saga:
import { call, put, takeLatest } from 'redux-saga/effects';
import { loadDocument, loadDocumentSuccess, loadDocumentFailure } from './documentSlice';
function* fetchDocumentSaga(action) {
try {
const response = yield call(fetch, `/api/documents/${action.payload}`);
const data = yield call([response, 'json']);
yield put(loadDocumentSuccess(data));
} catch (error) {
yield put(loadDocumentFailure(error.message));
}
}
export function* watchDocuments() {
yield takeLatest(loadDocument.type, fetchDocumentSaga);
}
Styled Componentsβ
Styles are defined with styled-components for encapsulation:
import styled from 'styled-components';
import { lighten } from 'polished';
export const Button = styled.button`
background-color: ${props => props.theme.primary};
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: ${props => lighten(0.1, props.theme.primary)};
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
Build and Webpackβ
Webpack Configurationβ
The webpack.config.js file configures compilation:
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
entry: {
form: ['react-app-polyfill/ie11', 'react-app-polyfill/stable', './src/index.jsx'],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
},
optimization: {
minimizer: [new TerserPlugin()],
},
resolve: {
extensions: ['.js', '.jsx'],
},
};
npm Scriptsβ
Build scripts are defined in package.json:
{
"scripts": {
"webpack": "cross-env NODE_ENV=production webpack",
"webpack:watch": "cross-env NODE_ENV=development webpack watch",
"gulp": "gulp",
"gulp:watch": "gulp watch",
"build": "npm run gulp && npm run webpack"
}
}
Usage:
# Production build
npm run webpack
# Watch mode (development)
npm run webpack:watch
# Complete build (CSS + JS)
npm run build
Integration with ASP.NETβ
Loading Bundlesβ
Webpack bundles are loaded in ASP.NET views:
@* ASP.NET MVC Page *@
<div id="react-root"></div>
<script src="~/Scripts/form/dist/form.bundle.js"></script>
<script>
// React initialization
FormApp.init({
documentId: '@Model.DocumentId',
formId: '@Model.FormId'
});
</script>
JavaScript β C# Communicationβ
Data is exchanged via:
Initial Data (C# β JS):
<script>
window.__INITIAL_STATE__ = @Html.Raw(Json.Encode(Model));
</script>
API Calls (JS β C#):
// Call to an ASP.NET endpoint
const response = await fetch('/api/documents/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(documentData),
});
Polyfills and Compatibilityβ
IE11 Supportβ
The application uses polyfills to support Internet Explorer 11:
import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import 'core-js/stable';
Polyfill Packages:
react-app-polyfill: React polyfills for IE11core-js: ES6+ polyfills@babel/polyfill: Babel polyfills
Hybrid Approachβ
Progressive Migrationβ
The architecture allows coexistence of:
Modern React Code:
- New components in React
- State management with Redux
- Styled components
Legacy Code:
- Existing jQuery components
- Knockout data-binding
- DevExpress controls
Interoperabilityβ
React components can interact with legacy code:
// React component calling jQuery code
import { useEffect } from 'react';
function LegacyComponent({ elementId }) {
useEffect(() => {
// Initialize a jQuery component
$(`#${elementId}`).datepicker({
dateFormat: 'dd/mm/yy'
});
return () => {
// Cleanup
$(`#${elementId}`).datepicker('destroy');
};
}, [elementId]);
return <div id={elementId}></div>;
}
Development Toolsβ
ESLintβ
ESLint configuration for code quality:
{
"extends": [
"airbnb",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:redux-saga/recommended",
"prettier"
],
"plugins": ["react", "react-hooks", "redux-saga"],
"rules": {
"react/prop-types": "warn",
"react-hooks/rules-of-hooks": "error",
"redux-saga/no-unhandled-errors": "error"
}
}
Prettierβ
Automatic code formatting:
{
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"semi": true
}
Jestβ
JavaScript unit tests:
{
"scripts": {
"test:js": "jest"
}
}
GraphQL (Future)β
β οΈ Note: GraphQL is not yet implemented in the current architecture, but could be added in the future to replace REST APIs.
Potential Benefits:
- Flexible client-side queries
- Strong typing with GraphQL schemas
- Reduction of over-fetching
- Automatic introspection
Envisioned Stack:
- Apollo Client: React GraphQL client
- HotChocolate: .NET GraphQL server
- GraphQL Code Generator: TypeScript generation
Best Practicesβ
Component Organizationβ
Functional Components with Hooks:
import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
function DocumentEditor({ documentId }) {
const dispatch = useDispatch();
const document = useSelector(state => state.documents.current);
const [isDirty, setIsDirty] = useState(false);
useEffect(() => {
dispatch(loadDocument(documentId));
}, [documentId, dispatch]);
// ...
}
Performanceβ
Memoization:
import { useMemo, useCallback } from 'react';
function ExpensiveComponent({ items }) {
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
const handleClick = useCallback(
(id) => console.log('Clicked:', id),
[]
);
// ...
}
Code Splitting:
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
Future Evolutionβ
Technical Roadmapβ
Short Term:
- Migration of jQuery components β React
- React component unit tests
- Webpack bundle optimization
Medium Term:
- TypeScript adoption for React
- GraphQL introduction
- Migration to React 18
Long Term:
- 100% React application
- Server-Side Rendering (SSR)
- Microfrontends